././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462640.9912324 librosa-1.0.0/0000755000175100017510000000000015236640661012622 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2034264 librosa-1.0.0/AUTHORS.md0000644000175100017510000001465115236640653014301 0ustar00runnerrunnerContributors ============ * Brian McFee * Matt McVicar * Dawen Liang * Colin Raffel * Douglas Repetto * Dan Ellis * Eric Battenberg * Bryce Walburn * Oriol Nieto * Romain Hennequin * Ryuichi Yamamoto * Rachel Bittner * Stefan Balke * CJ Carr * Fabian Robert-Stöter * Keunwoo Choi * Eric Humphrey * Carl Thomé * Seth Kranzler * Waldir Pimenta * Hojin Lee * Paul Brossier * Rimvydas Naktinis * Jackie Wu * Erik Peterson * Siddhartha Kumar * Julia Wilkins * Curtis Hawthorne * Dana Lee * Pius Friesch * Ziyao Wei * Vincent Lostanlen * Steve Tjoa * Matt Vollrath * Zhen Wang * Kyungyun Lee * Frank Zalkow * Shayenne Moura * nullmightybofo * toddrme2178 * Andy Sarroff * Monsij Biswal * Taewoon Kim * Adam Weiss * Viktor Andreevitch Morozov * Jan Schlüter * Darío Hereñú * Alex Metsai * Jack Mason * Scott Seyfarth * Voodoohop * Alastair Porter * Fabian Keller * Ayoub Malek * Stef van der Struijk * Shunsuke Hidaka * Eric Semeniuc * Frank Cwitkowitz * Bea Steers * beantowel * Thor Whalen * Stefan Sullivan * Benjamin van Niekirk * Kaushik Sripathi Hatti * Kaustuv Kanti Ganguli * Dan Mazur * Joon Lim * Pavel Campr * Tim Gates * Abduttayyeb Mazhar * Cash TingHin Lo * Xinlu Zhuang * Connor Guerrero * N. Dorukhan Sergin * Paul Biberstein * Myungchul Keum * Daniel Faronbi * Iran Roman * philstem * Alex Malins * i-aky-y * Asmitha Krishnakumar * Chandrashekhar Ramaprasad * Xiao-Ming * Will Monroe * Lorenz Nickel * Mattia Di Gangi * Kian Eliasi * Anton Kostin * Emily Halvachs * Juanita Gomez * Helmi Jinoz * Zvi Baratz * Alexandre Chabot-Leclerc * Steve Kowalik * Tom Xi * Matan Gover * Fabian Keller * BdeGraff * Jon Petter Ã…sen * Shin Hyun * Iliya S * Eugene Rabinovich * Gao Lei * Jize Guo * Phillip S.M. Skelton * Matt Pitkin * Anmol Mishra * Slava Chaunin * BenedictSt * Scott VanRavenswaay * David Südholt * Suhas Holla Karkada Chandrashekar * Gopesh Pandey * Daniel Fernandes * Ethan Cajetan Menezes * Valerian Coelho * Imrul Huda * ejwong * AD * Daniel Haas B * Joren Hammudoglu * Cameron Brooks * Petra Kuhnle * cookesan * Haydn Lam * Fadel Akram * emilazy * Ale Lloveras * Kayvan Zahiri Some feature extraction code was based on by Ron Weiss. Large portions of librosa were ported from MATLAB code by Dan Ellis . Some spectral features were ported from original MATLAB implementations by Erik Schmidt and Jeff Scott at the Drexel METlab Additional thanks to Emanuele Coviello and Erik Schmidt for testing, bug reports and fixes. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2034264 librosa-1.0.0/CODE_OF_CONDUCT.md0000644000175100017510000000622015236640653015422 0ustar00runnerrunner# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at brian.mcfee@nyu.edu. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2034264 librosa-1.0.0/CONTRIBUTING.md0000644000175100017510000001534015236640653015057 0ustar00runnerrunner Contributing ============ The preferred way to contribute to librosa is to fork the [main repository](https://github.com/librosa/librosa/) on GitHub: 1. Fork the [project repository](https://github.com/librosa/librosa): click on the 'Fork' button near the top of the page. This creates a copy of the code under your account on the GitHub server. 2. Clone this copy to your local disk: ```shell git clone git@github.com:YourLogin/librosa.git ``` 3. Set the upstream remote to the Librosa's repo: ```shell git remote add upstream git@github.com:librosa/librosa.git ``` 4. Create a new conda environment in order to install dependencies: ```shell conda create -n librosa-dev python=3.13 conda env update -n librosa-dev --file .github/environment-ci.yml conda activate librosa-dev python -m pip install -e '.[tests]' ``` 5. Create a branch to hold your changes: ```shell git switch -c ``` and start making changes. Never work in the ``main`` branch! 6. Work on this copy on your computer using Git to do the version control. You can check your modified files using: ```shell git status ``` 7. When you're done editing, do: ```shell git add git commit -m "" ``` to record your changes in Git, then push them to GitHub with: ```shell git push --set-upstream origin ``` 8. Go to the web page of your fork of the librosa repo, and click 'Pull request' to review your changes and add a description of what you did. Finally, click 'Create pull request' to send your changes to the maintainers for review. This will send an email to the committers. (If any of the above seems like magic to you, then look up the [Git documentation](https://git-scm.com/docs) on the web.) It is recommended to check that your contribution complies with the following rules before submitting a pull request: - All functions should have informative [docstrings](https://numpydoc.readthedocs.io/en/latest/format.html) with sample usage presented. You can also check for common programming errors with the following tools: - Code with good test coverage, check with: ```shell pytest ``` - Style adherence, check with: ```shell conda install ruff ruff check librosa ``` - Ensure that your docstrings are properly formatted, check with: ```shell conda install numpydoc numpydoc lint librosa/**.py ``` - Ensure that any new functionality has valid type annotations, check with: ```shell python -m pip install mypy python -m mypy librosa ``` Some tests in tests/test_display.py use baseline images for output comparison. If existing images need to be updated or new ones should be added: 1. Ensure that the environment is properly setup for testing (pytest with addons) 2. Run: ```shell pytest --mpl-generate-path=tmp tests/test_display.py [-k ANY_TESTS_THAT_CHANGED] ``` 3. Inspect the new baseline images under tmp/ 4. If (3) looks good, copy into `tests/baseline_images/test_display/` and add to the PR. Additionally, some functions require network access to test properly. If you are adding tests that require network access, please mark them with `@pytest.mark.network`. Filing bugs ----------- We use Github issues to track all bugs and feature requests; feel free to open an issue if you have found a bug or wish to see a feature implemented. It is recommended to check that your issue complies with the following rules before submitting: - Verify that your issue is not being currently addressed by other [issues](https://github.com/librosa/librosa/issues?q=) or [pull requests](https://github.com/librosa/librosa/pulls?q=). - Please ensure all code snippets and error messages are formatted in appropriate code blocks. See [Creating and highlighting code blocks](https://help.github.com/articles/creating-and-highlighting-code-blocks). - Please include your operating system type and version number, as well as your Python, librosa, numpy, and scipy versions. This information can be found by running the following code snippet: ```python import platform; print(platform.platform()) import sys; print("Python", sys.version) import numpy; print("NumPy", numpy.__version__) import scipy; print("SciPy", scipy.__version__) import librosa; print("librosa", librosa.__version__) ``` Documentation ------------- You can edit the documentation using any text editor and then generate the HTML output by typing ``make html`` from the docs/ directory. The resulting HTML files will be placed in _build/html/ and are viewable in a web browser. For building the documentation, you will need some additional dependencies. These can be installed by executing the following command: ```shell python -m pip install -e '.[docs]' ``` Policy on Using AI Assistance (Copilot, Gemini, ChatGPT, etc.) -------------------------------------------------------------- We do not micromanage your development workspace, nor do we require you to track every prompt or model version used while brainstorming. If you use generative AI as an advanced search engine, a pair programmer, a rubber duck to debug tricky math, or a helper to build a boilerplate utility script, you are completely free to do so. Instead of administrative tracking, we ask that you follow these principles in developing contributions: ### 1. The Accountability and Explainability Standard You are the author of your Pull Request, not the AI. If a maintainer asks why your code is the way it is, replying "that's just what the AI produced" is not acceptable. You must completely understand, be able to explain, and technically stand behind every line of code you submit. ### 2. The Verification Imperative If an AI assistant was used to generate code blocks, data parsers, regular expressions, or core signal processing logic, ensure that the solution falls into a category where correctness can be confidently and efficiently verified. ### 3. No AI-Generated Repository Dialogue While AI is welcome in your private code-generation loop, it is not welcome in our community dialogue space. We (the maintainers) do reserve the right to use our own automation and AI tools in generating issues, pull requests, or comments. We **do not** invite external AI contributions of this form however, as they generate a significant amount of work for us maintainers to process. Note ---- This document was gleefully adapted from [scikit-learn](https://scikit-learn.org/). The AI policy was generated in part by Gemini 3.5 Flash as a synthesis of similar policies indexed by [@melissawm](https://github.com/melissawm/open-source-ai-contribution-policies). Yes, we appreciate the irony in that, and the initial draft has been heavily revised and edited by human maintainers. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2034264 librosa-1.0.0/LICENSE.md0000644000175100017510000000137615236640653014236 0ustar00runnerrunner## ISC License Copyright (c) 2013--2026, librosa development team. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2034264 librosa-1.0.0/MANIFEST.in0000644000175100017510000000025215236640653014360 0ustar00runnerrunnerinclude AUTHORS.md include CONTRIBUTING.md include LICENSE.md include CODE_OF_CONDUCT.md include librosa/util/example_data/* global-include *.pyi global-include py.typed ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462640.9903185 librosa-1.0.0/PKG-INFO0000644000175100017510000001505315236640661013723 0ustar00runnerrunnerMetadata-Version: 2.4 Name: librosa Version: 1.0.0 Summary: Python module for audio and music processing Home-page: https://librosa.org Author: Brian McFee, librosa development team Author-email: brian.mcfee@nyu.edu License: ISC Project-URL: Documentation, https://librosa.org/doc Project-URL: Download, https://github.com/librosa/librosa/releases Project-URL: Source, https://github.com/librosa/librosa Project-URL: Tracker, https://github.com/librosa/librosa/issues Project-URL: Discussion forum, https://groups.google.com/g/librosa Classifier: License :: OSI Approved :: ISC License (ISCL) Classifier: Programming Language :: Python Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis Classifier: Framework :: Matplotlib Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.14 Requires-Python: >=3.12 Description-Content-Type: text/markdown; charset=UTF-8 License-File: LICENSE.md Requires-Dist: numba>=0.61.0 Requires-Dist: numpy>=2.1.0 Requires-Dist: scipy>=1.15.0 Requires-Dist: scikit-learn>=1.6.0 Requires-Dist: joblib>=1.2 Requires-Dist: decorator>=5.2.1 Requires-Dist: soundfile>=0.12.1 Requires-Dist: pooch>=1.7 Requires-Dist: soxr>=1.0.0 Requires-Dist: lazy_loader>=0.3 Requires-Dist: msgpack>=1.0.5 Provides-Extra: docs Requires-Dist: numpydoc>=1.8.0; extra == "docs" Requires-Dist: sphinx!=1.3.1; extra == "docs" Requires-Dist: matplotlib>=3.10.0; extra == "docs" Requires-Dist: pydata-sphinx-theme>=0.18.0; extra == "docs" Requires-Dist: sphinx-gallery>=0.7; extra == "docs" Requires-Dist: sphinx-design>=0.7.0; extra == "docs" Requires-Dist: mir_eval>=0.5; extra == "docs" Requires-Dist: ipython>=7.0; extra == "docs" Requires-Dist: sphinxcontrib-svg2pdfconverter; extra == "docs" Requires-Dist: sphinxcontrib-googleanalytics>=0.4; extra == "docs" Requires-Dist: presets; extra == "docs" Requires-Dist: sphinx-copybutton>=0.5.2; extra == "docs" Requires-Dist: myst-parser>=2.0; extra == "docs" Requires-Dist: pandas>=2.0; extra == "docs" Requires-Dist: umap-learn>=0.5.4; extra == "docs" Provides-Extra: tests Requires-Dist: matplotlib>=3.10.0; extra == "tests" Requires-Dist: packaging>=23.2; extra == "tests" Requires-Dist: pytest-mpl; extra == "tests" Requires-Dist: pytest-cov; extra == "tests" Requires-Dist: pytest; extra == "tests" Requires-Dist: samplerate; extra == "tests" Requires-Dist: resampy>=0.4.3; extra == "tests" Requires-Dist: scipy-stubs; extra == "tests" Requires-Dist: types-decorator; extra == "tests" Provides-Extra: display Requires-Dist: matplotlib>=3.10.0; extra == "display" Dynamic: license-file [![librosa logo](docs/img/librosa_logo_text.svg)](https://librosa.org/) # librosa `librosa` is a Python library for audio and music signal processing. It provides the foundational algorithms and tools required for building music information retrieval (MIR) systems. [![PyPI](https://img.shields.io/pypi/v/librosa.svg)](https://pypi.python.org/pypi/librosa) [![Anaconda-Server Badge](https://anaconda.org/conda-forge/librosa/badges/version.svg)](https://anaconda.org/conda-forge/librosa) [![License](https://img.shields.io/pypi/l/librosa.svg)](https://github.com/librosa/librosa/blob/main/LICENSE.md) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.591533.svg)](https://doi.org/10.5281/zenodo.591533) [![EffVer Versioning](https://img.shields.io/badge/version_scheme-EffVer-0097a7)](https://jacobtomlinson.dev/effver) [![Docs](https://github.com/librosa/librosa/actions/workflows/docs.yml/badge.svg)](https://librosa.org/doc/latest/index.html) [![Scientific Python Ecosystem Coordination](https://img.shields.io/badge/SPEC-0,1,7-green?labelColor=%23004811&color=%235CA038)](https://scientific-python.org/specs/) [![CI](https://github.com/librosa/librosa/actions/workflows/ci.yml/badge.svg)](https://github.com/librosa/librosa/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/librosa/librosa/branch/main/graph/badge.svg?token=ULWnUHaIJC)](https://codecov.io/gh/librosa/librosa) ## Documentation Full documentation is available at: * https://librosa.org/doc/ If you're new to librosa, we recommend starting with the [tutorials](https://librosa.org/doc/latest/auto_tutorials). If you're looking for API details, head directly to the [API reference](https://librosa.org/doc/latest/api). To see what's new, check out the [change log](https://librosa.org/doc/latest/changelog.html). ## Installation ### Using PyPI The latest stable release is available on PyPI, and you can install it by the command ``` pip install librosa ``` ### Using conda Anaconda users can install using `conda-forge`: ``` conda install -c conda-forge librosa ``` For optional dependencies and advanced setup, see the guide in the documentation. --- ## Citing There are two ways to cite librosa in scholarly work, depending on whether you are citing it for its use in your work or for its design and motivation. We additionally encourage you to cite the original publications describing the methods implemented in librosa, in addition to citing librosa itself. References can often be found within function documentation. ### Citing for use If you are citing for use in your own work, please cite the version you used by retrieving the appropriate DOI and citation information from Zenodo. [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.591533.svg)](https://doi.org/10.5281/zenodo.591533) This ensures that all contributing authors are credited for the work you are building on. From librosa version 0.10.2 or later, you can also use `librosa.cite()` to get the DOI link for any version of librosa: ```python import librosa librosa.cite() ``` And from 1.0 onward, you can directly retrieve the BibTeX entry for the version you are using: ```python print(librosa.cite(bib=True)) ``` ### Citing for design If you wish to cite librosa for its design, motivation, etc., please cite the paper published at SciPy 2015: McFee, Brian, Colin Raffel, Dawen Liang, Daniel PW Ellis, Matt McVicar, Eric Battenberg, and Oriol Nieto. "librosa: Audio and music signal analysis in python." In Proceedings of the 14th python in science conference, pp. 18-25. 2015. As a rule of thumb: - Use a versioned librosa citation to document the software implementation used in your experiments. - Cite the original methodological papers for the algorithms that are central to your work. - Cite the SciPy paper when discussing librosa itself, its design, or its contribution as a software library. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2034264 librosa-1.0.0/README.md0000644000175100017510000000750115236640653014105 0ustar00runnerrunner[![librosa logo](docs/img/librosa_logo_text.svg)](https://librosa.org/) # librosa `librosa` is a Python library for audio and music signal processing. It provides the foundational algorithms and tools required for building music information retrieval (MIR) systems. [![PyPI](https://img.shields.io/pypi/v/librosa.svg)](https://pypi.python.org/pypi/librosa) [![Anaconda-Server Badge](https://anaconda.org/conda-forge/librosa/badges/version.svg)](https://anaconda.org/conda-forge/librosa) [![License](https://img.shields.io/pypi/l/librosa.svg)](https://github.com/librosa/librosa/blob/main/LICENSE.md) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.591533.svg)](https://doi.org/10.5281/zenodo.591533) [![EffVer Versioning](https://img.shields.io/badge/version_scheme-EffVer-0097a7)](https://jacobtomlinson.dev/effver) [![Docs](https://github.com/librosa/librosa/actions/workflows/docs.yml/badge.svg)](https://librosa.org/doc/latest/index.html) [![Scientific Python Ecosystem Coordination](https://img.shields.io/badge/SPEC-0,1,7-green?labelColor=%23004811&color=%235CA038)](https://scientific-python.org/specs/) [![CI](https://github.com/librosa/librosa/actions/workflows/ci.yml/badge.svg)](https://github.com/librosa/librosa/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/librosa/librosa/branch/main/graph/badge.svg?token=ULWnUHaIJC)](https://codecov.io/gh/librosa/librosa) ## Documentation Full documentation is available at: * https://librosa.org/doc/ If you're new to librosa, we recommend starting with the [tutorials](https://librosa.org/doc/latest/auto_tutorials). If you're looking for API details, head directly to the [API reference](https://librosa.org/doc/latest/api). To see what's new, check out the [change log](https://librosa.org/doc/latest/changelog.html). ## Installation ### Using PyPI The latest stable release is available on PyPI, and you can install it by the command ``` pip install librosa ``` ### Using conda Anaconda users can install using `conda-forge`: ``` conda install -c conda-forge librosa ``` For optional dependencies and advanced setup, see the guide in the documentation. --- ## Citing There are two ways to cite librosa in scholarly work, depending on whether you are citing it for its use in your work or for its design and motivation. We additionally encourage you to cite the original publications describing the methods implemented in librosa, in addition to citing librosa itself. References can often be found within function documentation. ### Citing for use If you are citing for use in your own work, please cite the version you used by retrieving the appropriate DOI and citation information from Zenodo. [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.591533.svg)](https://doi.org/10.5281/zenodo.591533) This ensures that all contributing authors are credited for the work you are building on. From librosa version 0.10.2 or later, you can also use `librosa.cite()` to get the DOI link for any version of librosa: ```python import librosa librosa.cite() ``` And from 1.0 onward, you can directly retrieve the BibTeX entry for the version you are using: ```python print(librosa.cite(bib=True)) ``` ### Citing for design If you wish to cite librosa for its design, motivation, etc., please cite the paper published at SciPy 2015: McFee, Brian, Colin Raffel, Dawen Liang, Daniel PW Ellis, Matt McVicar, Eric Battenberg, and Oriol Nieto. "librosa: Audio and music signal analysis in python." In Proceedings of the 14th python in science conference, pp. 18-25. 2015. As a rule of thumb: - Use a versioned librosa citation to document the software implementation used in your experiments. - Cite the original methodological papers for the algorithms that are central to your work. - Cite the SciPy paper when discussing librosa itself, its design, or its contribution as a software library. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1786462640.979609 librosa-1.0.0/librosa/0000755000175100017510000000000015236640661014255 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2087643 librosa-1.0.0/librosa/__init__.py0000644000175100017510000000105115236640653016364 0ustar00runnerrunner#!/usr/bin/env python # -*- coding: utf-8 -*- """ Librosa is a python package for music and audio analysis. It provides the building blocks necessary to create audio retrieval systems. Core functionality (loading audio, spectral representations, etc.) is accessible from the top level of the package, while more specialized functionality (beat tracking, visualization, etc.) is organized into submodules. """ import lazy_loader as lazy from .version import version as __version__ __getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2095845 librosa-1.0.0/librosa/__init__.pyi0000644000175100017510000001160515236640653016543 0ustar00runnerrunnerfrom . import beat, core, decompose, display, effects, feature, filters, onset, segment, sequence, util from ._cache import cache as cache from .core import A4_to_tuning as A4_to_tuning from .core import A_weighting as A_weighting from .core import B_weighting as B_weighting from .core import C_weighting as C_weighting from .core import D_weighting as D_weighting from .core import Z_weighting as Z_weighting from .core import amplitude_to_db as amplitude_to_db from .core import autocorrelate as autocorrelate from .core import blocks_to_frames as blocks_to_frames from .core import blocks_to_samples as blocks_to_samples from .core import blocks_to_time as blocks_to_time from .core import chirp as chirp from .core import clicks as clicks from .core import cqt as cqt from .core import cqt_frequencies as cqt_frequencies from .core import db_to_amplitude as db_to_amplitude from .core import db_to_power as db_to_power from .core import estimate_tuning as estimate_tuning from .core import f0_harmonics as f0_harmonics from .core import fft_frequencies as fft_frequencies from .core import fifths_to_note as fifths_to_note from .core import fmt as fmt from .core import fourier_tempo_frequencies as fourier_tempo_frequencies from .core import frames_to_samples as frames_to_samples from .core import frames_to_time as frames_to_time from .core import frequency_weighting as frequency_weighting from .core import get_duration as get_duration from .core import get_samplerate as get_samplerate from .core import griffinlim as griffinlim from .core import griffinlim_cqt as griffinlim_cqt from .core import hybrid_cqt as hybrid_cqt from .core import hz_to_fjs as hz_to_fjs from .core import hz_to_mel as hz_to_mel from .core import hz_to_midi as hz_to_midi from .core import hz_to_note as hz_to_note from .core import hz_to_octs as hz_to_octs from .core import hz_to_svara_c as hz_to_svara_c from .core import hz_to_svara_h as hz_to_svara_h from .core import icqt as icqt from .core import iirt as iirt from .core import interp_harmonics as interp_harmonics from .core import interval_frequencies as interval_frequencies from .core import interval_to_fjs as interval_to_fjs from .core import istft as istft from .core import key_to_degrees as key_to_degrees from .core import key_to_notes as key_to_notes from .core import list_mela as list_mela from .core import list_thaat as list_thaat from .core import load as load from .core import loadx as loadx from .core import lpc as lpc from .core import magphase as magphase from .core import mel_frequencies as mel_frequencies from .core import mel_to_hz as mel_to_hz from .core import mela_to_degrees as mela_to_degrees from .core import mela_to_svara as mela_to_svara from .core import midi_to_hz as midi_to_hz from .core import midi_to_note as midi_to_note from .core import midi_to_svara_c as midi_to_svara_c from .core import midi_to_svara_h as midi_to_svara_h from .core import mu_compress as mu_compress from .core import mu_expand as mu_expand from .core import multi_frequency_weighting as multi_frequency_weighting from .core import note_to_hz as note_to_hz from .core import note_to_midi as note_to_midi from .core import note_to_svara_c as note_to_svara_c from .core import note_to_svara_h as note_to_svara_h from .core import octs_to_hz as octs_to_hz from .core import pcen as pcen from .core import perceptual_weighting as perceptual_weighting from .core import phase_vocoder as phase_vocoder from .core import piptrack as piptrack from .core import pitch_tuning as pitch_tuning from .core import plimit_intervals as plimit_intervals from .core import power_to_db as power_to_db from .core import pseudo_cqt as pseudo_cqt from .core import pyin as pyin from .core import pythagorean_intervals as pythagorean_intervals from .core import reassigned_spectrogram as reassigned_spectrogram from .core import resample as resample from .core import salience as salience from .core import samples_like as samples_like from .core import samples_to_frames as samples_to_frames from .core import samples_to_time as samples_to_time from .core import stft as stft from .core import stream as stream from .core import tempo_frequencies as tempo_frequencies from .core import thaat_to_degrees as thaat_to_degrees from .core import time_to_frames as time_to_frames from .core import time_to_samples as time_to_samples from .core import times_like as times_like from .core import to_mono as to_mono from .core import to_multi as to_multi from .core import to_stereo as to_stereo from .core import tone as tone from .core import tuning_to_A4 as tuning_to_A4 from .core import vqt as vqt from .core import yin as yin from .core import zero_crossings as zero_crossings from .util.exceptions import LibrosaError as LibrosaError from .util.exceptions import ParameterError as ParameterError from .util.files import cite as cite from .util.files import ex as ex from .util.files import example as example from .version import show_versions as show_versions ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2095845 librosa-1.0.0/librosa/_cache.py0000644000175100017510000000533215236640653016035 0ustar00runnerrunner#!/usr/bin/env python # -*- coding: utf-8 -*- """Function caching""" import os from typing import Any, Callable, TypeVar from decorator import FunctionMaker from joblib import Memory def _decorator_apply(dec, func): return FunctionMaker.create( func, "return decfunc(%(shortsignature)s)", dict(decfunc=dec(func)), __wrapped__=func, ) _F = TypeVar("_F", bound=Callable[..., Any]) class CacheManager: """The librosa cache manager class wraps joblib.Memory with a __call__ attribute, so that it may act as a function. Additionally, it provides a caching level filter, so that different functions can be cached or not depending on the user's preference for speed vs. storage usage. """ def __init__(self, *args: Any, **kwargs: Any): level = kwargs.pop("level", 10) # Initialize the memory object self.memory: Memory = Memory(*args, **kwargs) # The level parameter controls which data we cache # smaller numbers mean less caching self.level: int = level def __call__(self, level: int) -> Callable[[_F], _F]: """ Cache with an explicitly defined level. Example usage: @cache(level=2) def semi_important_function(some_arguments): ... """ def wrapper(function): """Add an input/output cache to the specified function.""" if self.memory.location is not None and self.level >= level: return _decorator_apply(self.memory.cache, function) else: return function return wrapper def clear(self, *args: Any, **kwargs: Any) -> None: """Clear the cache""" self.memory.clear(*args, **kwargs) def eval(self, *args: Any, **kwargs: Any) -> Any: """Evaluate a function""" return self.memory.eval(*args, **kwargs) def format(self, *args: Any, **kwargs: Any) -> Any: """Return the formatted representation of an object""" return self.memory.format(*args, **kwargs) def reduce_size(self, *args: Any, **kwargs: Any) -> None: """Reduce the size of the cache""" self.memory.reduce_size(*args, **kwargs) # pragma: no cover def warn(self, *args: Any, **kwargs: Any) -> None: """Raise a warning""" self.memory.warn(*args, **kwargs) # pragma: no cover # Instantiate the cache from the environment cache: CacheManager = CacheManager( os.environ.get("LIBROSA_CACHE_DIR", None), mmap_mode=os.environ.get("LIBROSA_CACHE_MMAP", None), compress=os.environ.get("LIBROSA_CACHE_COMPRESS", False), verbose=int(os.environ.get("LIBROSA_CACHE_VERBOSE", 0)), level=int(os.environ.get("LIBROSA_CACHE_LEVEL", 10)), ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2095845 librosa-1.0.0/librosa/_typing.py0000644000175100017510000000654615236640653016314 0ustar00runnerrunner# ruff: noqa: PYI047 from collections.abc import Sequence from typing import Any, Callable, Generator, Literal, Never import numpy as np import scipy.sparse as sp from numpy.typing import ArrayLike # RNG types type SeedLike = int | np.integer | Sequence[int] | np.random.SeedSequence type RNGLike = np.random.Generator | np.random.BitGenerator type _WindowSpec = str | tuple[Any, ...] | float | Callable[[int], np.ndarray] | ArrayLike type _IterableLike[T] = list[T] | tuple[T, ...] | Generator[T, None, None] type _SequenceLike[T] = Sequence[T] | np.ndarray type _ScalarOrSequence[T] = T | _SequenceLike[T] # The following definitions are copied from numpy/_typing/_scalars.py # (We don't import them directly from numpy because they're an implementation detail.) ### ### START COPIED CODE ### type _CharLike_co = str | bytes # The `Like_co` type-aliases below represent all scalars that can be # coerced into `` (with the casting rule `same_kind`) type _BoolLike_co = bool | np.bool type _UIntLike_co = bool | np.unsignedinteger | np.bool type _IntLike_co = int | np.integer | np.bool type _FloatLike_co = float | np.floating | np.integer | np.bool type _ComplexLike_co = complex | np.number | np.bool type _NumberLike_co = _ComplexLike_co type _TD64Like_co = int | np.timedelta64 | np.integer | np.bool # `_VoidLike_co` is technically not a scalar, but it's close enough type _VoidLike_co = tuple[Any, ...] | np.void type _ScalarLike_co = complex | str | bytes | np.generic # Padding modes in general type _ModeKind = Literal[ "constant", "edge", "linear_ramp", "maximum", "mean", "median", "minimum", "reflect", "symmetric", "wrap", "empty", ] ### ### END COPIED CODE ### # Padding modes for head/tail padding # These rule out padding modes that depend on the entire array type _STFTPad = Literal[ "constant", "edge", "linear_ramp", "reflect", "symmetric", "empty", ] type _PadMode = _ModeKind | Callable[..., Any] type _PadModeSTFT = _STFTPad | Callable[..., Any] def _ensure_not_reachable(*, __arg: Never): """ Ensure that a code path is not reachable, like typing_extension.assert_never. This doesn't raise an exception so that we are forced to manually raise a more user friendly exception afterwards. """ ... type _SparseMatrix = ( sp.bsr_matrix | sp.coo_matrix | sp.csc_matrix | sp.csr_matrix | sp.dia_matrix | sp.dok_matrix | sp.lil_matrix ) type _SparseArray = ( sp.bsr_array | sp.coo_array | sp.csc_array | sp.csr_array | sp.dia_array | sp.dok_array | sp.lil_array ) # matches the `interp` argument in `scipy.interpolate.interp1d` on all supported SciPy versions # https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.interp1d.html type _InterpKind = Literal[ "linear", "nearest", "nearest-up", "zero", "slinear", "quadratic", "cubic", "previous", "next", ] # DCT normalization modes type _DCTNorm = Literal["backward", "ortho", "forward"] type _DCTType = Literal[1, 2, 3, 4] # More specialized number types type _Real = float | np.integer[Any] | np.floating[Any] type _Complex = _Real | np.complexfloating[Any, Any] type _Number = complex | np.number[Any] # Shape-typing type _Array1D[ScalarT: np.generic] = np.ndarray[tuple[int], np.dtype[ScalarT]] type _Array2D[ScalarT: np.generic] = np.ndarray[tuple[int, int], np.dtype[ScalarT]] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2095845 librosa-1.0.0/librosa/beat.py0000644000175100017510000006454715236640653015563 0ustar00runnerrunner#!/usr/bin/env python # -*- coding: utf-8 -*- """The librosa.beat module provides two functions for estimating beat locations from an audio signal.""" from __future__ import annotations from typing import TYPE_CHECKING, Any, Literal, overload import numba import numpy as np import scipy from . import core, onset, util from .feature import fourier_tempogram from .feature import tempo as _tempo from .util.exceptions import ParameterError if TYPE_CHECKING: import numpy.typing as npt import scipy.stats from typing_extensions import TypeVar from ._typing import _Array1D type _BpmLike = ( float | np.floating[Any] | np.integer[Any] | npt.NDArray[np.floating[Any]] ) # `typing_extensions.TypeVar` is only needed until Python 3.13, which adds support # for type parameter defaults (PEP 696). _SparseBpmT = TypeVar("_SparseBpmT", bound=_BpmLike, default=float | npt.NDArray[np.float64]) _DenseBpmT = TypeVar("_DenseBpmT", bound=_BpmLike, default=npt.NDArray[np.float64]) __all__ = ["beat_track", "plp"] @overload # units="frames" | "samples", sparse=True (default) def beat_track( *, y: npt.NDArray[np.floating[Any]] | None = None, sr: float = 22050, onset_envelope: npt.NDArray[np.floating[Any]] | None = None, hop_length: int = 512, start_bpm: float = 120.0, tightness: float = 100, trim: bool = True, bpm: _SparseBpmT | None = None, prior: scipy.stats.rv_continuous | None = None, units: Literal["frames", "samples"] = "frames", sparse: Literal[True] = True, ) -> tuple[_SparseBpmT, npt.NDArray[np.int_]]: ... @overload # units="time", sparse=True (default) def beat_track( *, y: npt.NDArray[np.floating[Any]] | None = None, sr: float = 22050, onset_envelope: npt.NDArray[np.floating[Any]] | None = None, hop_length: int = 512, start_bpm: float = 120.0, tightness: float = 100, trim: bool = True, bpm: _SparseBpmT | None = None, prior: scipy.stats.rv_continuous | None = None, units: Literal["time"], sparse: Literal[True] = True, ) -> tuple[_SparseBpmT, npt.NDArray[np.float64]]: ... @overload # sparse=False def beat_track( *, y: npt.NDArray[np.floating[Any]] | None = None, sr: float = 22050, onset_envelope: npt.NDArray[np.floating[Any]] | None = None, hop_length: int = 512, start_bpm: float = 120.0, tightness: float = 100, trim: bool = True, bpm: _DenseBpmT | None = None, prior: scipy.stats.rv_continuous | None = None, units: Literal["frames", "samples", "time"] = "frames", sparse: Literal[False], ) -> tuple[_DenseBpmT, npt.NDArray[np.bool]]: ... def beat_track( *, y: np.ndarray | None = None, sr: float = 22050, onset_envelope: np.ndarray | None = None, hop_length: int = 512, start_bpm: float = 120.0, tightness: float = 100, trim: bool = True, bpm: _BpmLike | None = None, prior: scipy.stats.rv_continuous | None = None, units: str = "frames", sparse: bool = True ) -> tuple[float | np.floating[Any] | np.integer[Any] | np.ndarray, np.ndarray]: r"""Dynamic programming beat tracker. Beats are detected in three stages, following the method of [#]_: 1. Measure onset strength 2. Estimate tempo from onset correlation 3. Pick peaks in onset strength approximately consistent with estimated tempo .. [#] Ellis, Daniel PW. "Beat tracking by dynamic programming." Journal of New Music Research 36.1 (2007): 51-60. http://labrosa.ee.columbia.edu/projects/beattrack/ Parameters ---------- y : np.ndarray [shape=(..., n)] or None Audio time series. sr : number > 0 [scalar] Sampling rate of ``y``. onset_envelope : np.ndarray [shape=(..., m)] or None (Optional) pre-computed onset strength envelope. hop_length : int > 0 [scalar] Number of audio samples between successive ``onset_envelope`` values. start_bpm : float > 0 [scalar] Initial guess for the tempo estimator (in beats per minute). tightness : float [scalar] Tightness of beat distribution around tempo. trim : bool [scalar] Trim leading/trailing beats with weak onsets. bpm : float [scalar] or np.ndarray [shape=(...)] (Optional) if provided, use ``bpm`` as the tempo instead of estimating it from ``onsets``. If multichannel, tempo estimates can be provided for all channels. Tempo estimates may also be time-varying, in which case the shape of ``bpm`` should match that of ``onset_envelope``, i.e., one estimate provided for each frame. prior : scipy.stats.rv_continuous [optional] An optional prior distribution over tempo. If provided, ``start_bpm`` will be ignored. units : {'frames', 'samples', 'time'} The units to encode detected beat events in. By default, 'frames' are used. sparse : bool If ``True`` (default), detections are returned as an array of frames, samples, or time indices (as specified by ``units=``). If ``False``, detections are encoded as a dense boolean array where ``beats[..., n]`` is true if there's a beat at frame index ``n``. .. note:: multi-channel input is only supported when ``sparse=False``. Returns ------- tempo : float [scalar, non-negative] or np.ndarray The estimated global tempo (in beats per minute). If multi-channel and ``bpm`` is not provided, a separate tempo will be returned for each channel. .. note:: By default, the tempo is returned as an ndarray even for mono input. In this case, the array will have a single element and be one-dimensional. This is to ensure consistent return types for multi-channel input. beats : np.ndarray The estimated beat event locations. If `sparse=True` (default), beat locations are given in the specified units (default is frame indices). If `sparse=False` (required for multichannel input), beat events are indicated by a boolean for each frame. .. note:: If no onset strength could be detected, beat_tracker estimates 0 BPM and returns an empty list. Raises ------ ParameterError if neither ``y`` nor ``onset_envelope`` are provided, or if ``units`` is not one of 'frames', 'samples', or 'time' See Also -------- librosa.onset.onset_strength Examples -------- Track beats using time series input >>> y, sr = librosa.loadx('choice', duration=10) >>> tempo, beats = librosa.beat.beat_track(y=y, sr=sr) >>> tempo 135.99917763157896 Print the frames corresponding to beats >>> beats array([ 3, 21, 40, 59, 78, 96, 116, 135, 154, 173, 192, 211, 230, 249, 268, 287, 306, 325, 344, 363]) Or print them as timestamps >>> librosa.frames_to_time(beats, sr=sr) array([0.07 , 0.488, 0.929, 1.37 , 1.811, 2.229, 2.694, 3.135, 3.576, 4.017, 4.458, 4.899, 5.341, 5.782, 6.223, 6.664, 7.105, 7.546, 7.988, 8.429]) Output beat detections as a boolean array instead of frame indices >>> tempo, beats_dense = librosa.beat.beat_track(y=y, sr=sr, sparse=False) >>> beats_dense array([False, False, False, True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, ..., False, False, True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False]) Track beats using a pre-computed onset envelope >>> onset_env = librosa.onset.onset_strength(y=y, sr=sr, ... aggregate=np.median) >>> tempo, beats = librosa.beat.beat_track(onset_envelope=onset_env, ... sr=sr) >>> tempo 135.99917763157896 >>> beats array([ 3, 21, 40, 59, 78, 96, 116, 135, 154, 173, 192, 211, 230, 249, 268, 287, 306, 325, 344, 363]) Plot the beat events against the onset strength envelope >>> import matplotlib.pyplot as plt >>> hop_length = 512 >>> fig, ax = plt.subplots(nrows=2, sharex=True) >>> times = librosa.times_like(onset_env, sr=sr, hop_length=hop_length) >>> M = librosa.feature.melspectrogram(y=y, sr=sr, hop_length=hop_length) >>> librosa.display.specshow(M, vscale='dBFS[power]', ... y_axis='mel', x_axis='time', hop_length=hop_length, ... ax=ax[0]) >>> ax[0].label_outer() >>> ax[0].set(title='Mel spectrogram') >>> ax[1].plot(times, librosa.util.normalize(onset_env), ... label='Onset strength') >>> ax[1].vlines(times[beats], 0, 1, alpha=0.5, color='r', ... linestyle='--', label='Beats') >>> ax[1].legend() """ # First, get the frame->beat strength profile if we don't already have one if onset_envelope is None: if y is None: raise ParameterError("y or onset_envelope must be provided") onset_envelope = onset.onset_strength( y=y, sr=sr, hop_length=hop_length, aggregate=np.median ) if sparse and onset_envelope.ndim != 1: raise ParameterError(f"sparse=True (default) does not support " f"{onset_envelope.ndim}-dimensional inputs. " f"Either set sparse=False or convert the signal to mono.") # Do we have any onsets to grab? if not onset_envelope.any(): if sparse: return (0.0, np.array([], dtype=int)) else: return (np.zeros(shape=onset_envelope.shape[:-1], dtype=float), np.zeros_like(onset_envelope, dtype=bool)) # Estimate BPM if one was not provided if bpm is None: bpm = _tempo( onset_envelope=onset_envelope, sr=sr, hop_length=hop_length, start_bpm=start_bpm, prior=prior, ) # Ensure that tempo is in a shape that is compatible with vectorization _bpm = np.atleast_1d(bpm) bpm_expanded = util.expand_to(_bpm, ndim=onset_envelope.ndim, axes=range(_bpm.ndim)) # Then, run the tracker beats = __beat_tracker(onset_envelope, bpm_expanded, float(sr) / hop_length, tightness, trim) if sparse: beats = np.flatnonzero(beats) if units == "frames": pass elif units == "samples": return (bpm, core.frames_to_samples(beats, hop_length=hop_length)) elif units == "time": return (bpm, core.frames_to_time(beats, hop_length=hop_length, sr=sr)) else: raise ParameterError(f"Invalid unit type: {units}") return (bpm, beats) def plp[FloatT: np.floating[Any]]( *, y: npt.NDArray[FloatT] | None = None, sr: float = 22050, onset_envelope: npt.NDArray[FloatT] | None = None, hop_length: int = 512, win_length: int = 384, tempo_min: float | None = 30, tempo_max: float | None = 300, prior: scipy.stats.rv_continuous | None = None, ) -> npt.NDArray[FloatT]: """Predominant local pulse (PLP) estimation. [#]_ The PLP method analyzes the onset strength envelope in the frequency domain to find a locally stable tempo for each frame. These local periodicities are used to synthesize local half-waves, which are combined such that peaks coincide with rhythmically salient frames (e.g. onset events on a musical time grid). The local maxima of the pulse curve can be taken as estimated beat positions. This method may be preferred over the dynamic programming method of `beat_track` when the tempo is expected to vary significantly over time. Additionally, since `plp` does not require the entire signal to make predictions, it may be preferable when beat-tracking long recordings in a streaming setting. .. [#] Grosche, P., & Muller, M. (2011). "Extracting predominant local pulse information from music recordings." IEEE Transactions on Audio, Speech, and Language Processing, 19(6), 1688-1701. Parameters ---------- y : np.ndarray [shape=(..., n)] or None Audio time series. Multi-channel is supported. sr : number > 0 [scalar] Sampling rate of ``y``. onset_envelope : np.ndarray [shape=(..., n)] or None (Optional) pre-computed onset strength envelope. hop_length : int > 0 [scalar] Number of audio samples between successive ``onset_envelope`` values. win_length : int > 0 [scalar] Number of frames to use for tempogram analysis. By default, 384 frames (at ``sr=22050`` and ``hop_length=512``) corresponds to about 8.9 seconds. tempo_min, tempo_max : numbers > 0 [scalar], optional Minimum and maximum permissible tempo values. ``tempo_max`` must be at least ``tempo_min``. Set either (or both) to `None` to disable this constraint. prior : scipy.stats.rv_continuous [optional] A prior distribution over tempo (in beats per minute). By default, a uniform prior over ``[tempo_min, tempo_max]`` is used. Returns ------- pulse : np.ndarray, shape=[(..., n)] The estimated pulse curve. Maxima correspond to rhythmically salient points of time. If input is multi-channel, one pulse curve per channel is computed. See Also -------- beat_track librosa.onset.onset_strength librosa.feature.fourier_tempogram Examples -------- Visualize the PLP compared to an onset strength envelope. Both are normalized here to make comparison easier. >>> y, sr = librosa.loadx('brahms') >>> onset_env = librosa.onset.onset_strength(y=y, sr=sr) >>> pulse = librosa.beat.plp(onset_envelope=onset_env, sr=sr) >>> # Or compute pulse with an alternate prior, like log-normal >>> import scipy.stats >>> prior = scipy.stats.lognorm(loc=np.log(120), scale=120, s=1) >>> pulse_lognorm = librosa.beat.plp(onset_envelope=onset_env, sr=sr, ... prior=prior) >>> melspec = librosa.feature.melspectrogram(y=y, sr=sr) >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=3, sharex=True) >>> librosa.display.specshow(melspec, vscale='dBFS[power]', ... x_axis='time', y_axis='mel', ax=ax[0]) >>> ax[0].set(title='Mel spectrogram') >>> ax[0].label_outer() >>> ax[1].plot(librosa.times_like(onset_env), ... librosa.util.normalize(onset_env), ... label='Onset strength') >>> ax[1].plot(librosa.times_like(pulse), ... librosa.util.normalize(pulse), ... label='Predominant local pulse (PLP)') >>> ax[1].set(title='Uniform tempo prior [30, 300]') >>> ax[1].label_outer() >>> ax[2].plot(librosa.times_like(onset_env), ... librosa.util.normalize(onset_env), ... label='Onset strength') >>> ax[2].plot(librosa.times_like(pulse_lognorm), ... librosa.util.normalize(pulse_lognorm), ... label='Predominant local pulse (PLP)') >>> ax[2].set(title='Log-normal tempo prior, mean=120', xlim=[5, 20]) >>> ax[2].legend() >>> plt.show() PLP local maxima can be used as estimates of beat positions. >>> tempo, beats = librosa.beat.beat_track(onset_envelope=onset_env) >>> beats_plp = np.flatnonzero(librosa.util.localmax(pulse)) >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=2, sharex=True, sharey=True) >>> times = librosa.times_like(onset_env, sr=sr) >>> ax[0].plot(times, librosa.util.normalize(onset_env), ... label='Onset strength') >>> ax[0].vlines(times[beats], 0, 1, alpha=0.5, color='r', ... linestyle='--', label='Beats') >>> ax[0].legend() >>> ax[0].set(title='librosa.beat.beat_track') >>> ax[0].label_outer() >>> # Limit the plot to a 15-second window >>> times = librosa.times_like(pulse, sr=sr) >>> ax[1].plot(times, librosa.util.normalize(pulse), ... label='PLP') >>> ax[1].vlines(times[beats_plp], 0, 1, alpha=0.5, color='r', ... linestyle='--', label='PLP Beats') >>> ax[1].legend() >>> ax[1].set(title='librosa.beat.plp', xlim=[5, 20]) >>> ax[1].xaxis.set_major_formatter(librosa.display.TimeFormatter()) >>> plt.show() """ # Step 1: get the onset envelope if onset_envelope is None: onset_envelope = onset.onset_strength( y=y, sr=sr, hop_length=hop_length, aggregate=np.median ) if tempo_min is not None and tempo_max is not None and tempo_max <= tempo_min: raise ParameterError( f"tempo_max={tempo_max} must be larger than tempo_min={tempo_min}" ) # Step 2: get the fourier tempogram ftgram = fourier_tempogram( onset_envelope=onset_envelope, sr=sr, hop_length=hop_length, win_length=win_length, ) # Step 3: pin to the feasible tempo range tempo_frequencies = core.fourier_tempo_frequencies( sr=sr, hop_length=hop_length, win_length=win_length ) if tempo_min is not None: ftgram[..., tempo_frequencies < tempo_min, :] = 0 if tempo_max is not None: ftgram[..., tempo_frequencies > tempo_max, :] = 0 # reshape lengths to match dimension properly tempo_frequencies = util.expand_to(tempo_frequencies, ndim=ftgram.ndim, axes=-2) # Step 3: Discard everything below the peak ftmag = np.log1p(1e6 * np.abs(ftgram)) if prior is not None: ftmag += prior.logpdf(tempo_frequencies) peak_values = ftmag.max(axis=-2, keepdims=True) ftgram[ftmag < peak_values] = 0 # Normalize to keep only phase information ftgram /= util.tiny(ftgram) ** 0.5 + np.abs(ftgram.max(axis=-2, keepdims=True)) # Step 5: invert the Fourier tempogram to get the pulse pulse = core.istft( ftgram, hop_length=1, n_fft=win_length, length=onset_envelope.shape[-1] ) # Step 6: retain only the positive part of the pulse cycle pulse = np.clip(pulse, 0, None, pulse) # Return the normalized pulse return util.normalize(pulse, axis=-1) def __beat_tracker( onset_envelope: np.ndarray, bpm: np.ndarray, frame_rate: float, tightness: float, trim: bool ) -> _Array1D[np.bool]: """Track beats in an onset strength envelope. Parameters ---------- onset_envelope : np.ndarray [shape=(..., n,)] onset strength envelope bpm : float [scalar] or np.ndarray [shape=(...)] tempo estimate frame_rate : float [scalar] frame rate of the spectrogram (sr / hop_length, frames per second) tightness : float [scalar, positive] how closely do we adhere to bpm? trim : bool [scalar] trim leading/trailing beats with weak onsets? Returns ------- beats : np.ndarray [shape=(n,)] frame numbers of beat events """ if np.any(bpm <= 0): raise ParameterError(f"bpm={bpm} must be strictly positive") if tightness <= 0: raise ParameterError("tightness must be strictly positive") # TODO: this might be better accomplished with a np.broadcast_shapes check if bpm.shape[-1] not in (1, onset_envelope.shape[-1]): raise ParameterError(f"Invalid bpm shape={bpm.shape} does not match " f"onset envelope shape={onset_envelope.shape}") # convert bpm to frames per beat (rounded) # [frames / sec] * [60 sec / min] / [beat / min] = [frames / beat] frames_per_beat = np.round(frame_rate * 60.0 / bpm) # localscore is a smoothed version of AGC'd onset envelope localscore = np.empty_like(onset_envelope) __beat_local_score(__normalize_onsets(onset_envelope), frames_per_beat, localscore) # run the DP backlink, cumscore = __beat_track_dp(localscore, frames_per_beat, tightness) # Reconstruct the beat path from backlinks tail = __last_beat(cumscore) beats = np.zeros_like(onset_envelope, dtype=bool) __dp_backtrack(backlink, tail, beats) # Discard spurious trailing beats beats_trimmed = np.empty_like(beats) __trim_beats(localscore, beats, trim, beats_trimmed) return beats_trimmed # -- Helper functions for beat tracking def __normalize_onsets(onsets): """Normalize onset strength by its standard deviation""" norm = onsets.std(ddof=1, axis=-1, keepdims=True) return onsets / (norm + util.tiny(onsets)) @numba.guvectorize( "(t),(n)->(t)", nopython=True, cache=False) def __beat_local_score(onset_envelope, frames_per_beat, localscore): # This function essentially implements a same-mode convolution, # but also allows for a time-varying convolution-like filter to support dynamic tempo. N = len(onset_envelope) if len(frames_per_beat) == 1: # Static tempo mode # NOTE: when we can bump the minimum numba to 0.58, we can eliminate this branch and just use # np.convolve(..., mode='same') directly window = np.exp(-0.5 * (np.arange(-frames_per_beat[0], frames_per_beat[0] + 1) * 32.0 / frames_per_beat[0]) ** 2) K = len(window) # This is a vanilla same-mode convolution for i in range(len(onset_envelope)): localscore[i] = 0. # we need i + K // 2 - k < N ==> k > i + K //2 - N # and i + K // 2 - k >= 0 ==> k <= i + K // 2 for k in range(max(0, i + K // 2 - N + 1), min(i + K // 2, K)): localscore[i] += window[k] * onset_envelope[i + K//2 -k] elif len(frames_per_beat) == len(onset_envelope): # Time-varying tempo estimates # This isn't exactly a convolution anymore, since the filter is time-varying, but it's pretty close for i in range(len(onset_envelope)): window = np.exp(-0.5 * (np.arange(-frames_per_beat[i], frames_per_beat[i] + 1) * 32.0 / frames_per_beat[i]) ** 2) K = 2 * int(frames_per_beat[i]) + 1 localscore[i] = 0. for k in range(max(0, i + K // 2 - N + 1), min(i + K // 2, K)): localscore[i] += window[k] * onset_envelope[i + K // 2 - k] @numba.guvectorize( [ "void(float32[:], float32[:], float32, int32[:], float32[:])", "void(float64[:], float64[:], float32, int32[:], float64[:])", ], "(t),(n),()->(t),(t)", nopython=True, cache=True) def __beat_track_dp(localscore, frames_per_beat, tightness, backlink, cumscore): """Core dynamic program for beat tracking""" # Threshold for the first beat to exceed score_thresh = 0.01 * localscore.max() # Are we on the first beat? first_beat = True backlink[0] = -1 cumscore[0] = localscore[0] # If tv == 0, then tv * i will always be 0, so we only ever use frames_per_beat[0] # If tv == 1, then tv * i = i, so we use the time-varying FPB tv = int(len(frames_per_beat) > 1) for i, score_i in enumerate(localscore): best_score = - np.inf beat_location = -1 # Search over all possible predecessors to find the best preceding beat # NOTE: to provide time-varying tempo estimates, we replace # frames_per_beat[0] by frames_per_beat[i] in this loop body. for loc in range(i - round(frames_per_beat[tv * i] / 2), int(i - 2 * frames_per_beat[tv * i] - 1), - 1): # Once we're searching past the start, break out if loc < 0: break score = cumscore[loc] - tightness * (np.log(i - loc) - np.log(frames_per_beat[tv * i]))**2 if score > best_score: best_score = score beat_location = loc # Add the local score if beat_location >= 0: cumscore[i] = score_i + best_score else: # No back-link found, so just use the current score cumscore[i] = score_i # Special case the first onset. Stop if the localscore is small if first_beat and score_i < score_thresh: backlink[i] = -1 else: backlink[i] = beat_location first_beat = False @numba.guvectorize( "(t),(t),()->(t)", nopython=True, cache=True ) def __trim_beats(localscore, beats, trim, beats_trimmed): """Remove spurious leading and trailing beats from the detection array""" # Populate the trimmed beats array with the existing values beats_trimmed[:] = beats # Compute the threshold: 1/2 RMS of the smoothed beat envelope w = np.hanning(5) # Slicing here to implement same-mode convolution in older numba where # mode='same' is not yet supported smooth_boe = np.convolve(localscore[beats], w)[len(w)//2:len(localscore)+len(w)//2] # This logic is to preserve old behavior and always discard beats detected with oenv==0 if trim: threshold = 0.5 * ((smooth_boe**2).mean()**0.5) else: threshold = 0.0 # Suppress bad beats n = 0 while localscore[n] <= threshold: beats_trimmed[n] = False n += 1 n = len(localscore) - 1 while localscore[n] <= threshold: beats_trimmed[n] = False n -= 1 pass def __last_beat(cumscore): """Identify the position of the last detected beat""" # Use a masked array to support multidimensional statistics # We negate the mask here because of numpy masked array semantics mask = ~util.localmax(cumscore, axis=-1) masked_scores: np.ma.masked_array = np.ma.masked_array(data=cumscore, mask=mask) medians = np.ma.median(masked_scores, axis=-1) thresholds = 0.5 * np.ma.getdata(medians) # Also find the last beat positions tail = np.empty(shape=cumscore.shape[:-1], dtype=int) __last_beat_selector(cumscore, mask, thresholds, tail) return tail @numba.guvectorize( "(t),(t),()->()", nopython=True, cache=True ) def __last_beat_selector(cumscore, mask, threshold, out): """Vectorized helper to identify the last valid beat position: cumscore[n] > threshold and not mask[n] """ n = len(cumscore) - 1 out[0] = n while n >= 0: if not mask[n] and cumscore[n] >= threshold: out[0] = n break else: n -= 1 @numba.guvectorize( "(t),()->(t)", nopython=True, cache=True ) def __dp_backtrack(backlinks, tail, beats): """Populate the beat indicator array from a sequence of backlinks""" n = tail while n >= 0: beats[n] = True n = backlinks[n] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462640.9825552 librosa-1.0.0/librosa/core/0000755000175100017510000000000015236640661015205 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2095845 librosa-1.0.0/librosa/core/__init__.py0000644000175100017510000000026015236640653017315 0ustar00runnerrunner#!/usr/bin/env python # -*- coding: utf-8 -*- """Core IO and DSP functions""" import lazy_loader as lazy __getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2099245 librosa-1.0.0/librosa/core/__init__.pyi0000644000175100017510000001140615236640653017472 0ustar00runnerrunnerfrom .audio import autocorrelate as autocorrelate from .audio import chirp as chirp from .audio import clicks as clicks from .audio import get_duration as get_duration from .audio import get_samplerate as get_samplerate from .audio import load as load from .audio import loadx as loadx from .audio import lpc as lpc from .audio import mu_compress as mu_compress from .audio import mu_expand as mu_expand from .audio import resample as resample from .audio import stream as stream from .audio import to_mono as to_mono from .audio import to_multi as to_multi from .audio import to_stereo as to_stereo from .audio import tone as tone from .audio import zero_crossings as zero_crossings from .constantq import cqt as cqt from .constantq import griffinlim_cqt as griffinlim_cqt from .constantq import hybrid_cqt as hybrid_cqt from .constantq import icqt as icqt from .constantq import pseudo_cqt as pseudo_cqt from .constantq import vqt as vqt from .convert import A4_to_tuning as A4_to_tuning from .convert import A_weighting as A_weighting from .convert import B_weighting as B_weighting from .convert import C_weighting as C_weighting from .convert import D_weighting as D_weighting from .convert import Z_weighting as Z_weighting from .convert import blocks_to_frames as blocks_to_frames from .convert import blocks_to_samples as blocks_to_samples from .convert import blocks_to_time as blocks_to_time from .convert import cqt_frequencies as cqt_frequencies from .convert import fft_frequencies as fft_frequencies from .convert import fourier_tempo_frequencies as fourier_tempo_frequencies from .convert import frames_to_samples as frames_to_samples from .convert import frames_to_time as frames_to_time from .convert import frequency_weighting as frequency_weighting from .convert import hz_to_fjs as hz_to_fjs from .convert import hz_to_mel as hz_to_mel from .convert import hz_to_midi as hz_to_midi from .convert import hz_to_note as hz_to_note from .convert import hz_to_octs as hz_to_octs from .convert import hz_to_svara_c as hz_to_svara_c from .convert import hz_to_svara_h as hz_to_svara_h from .convert import mel_frequencies as mel_frequencies from .convert import mel_to_hz as mel_to_hz from .convert import midi_to_hz as midi_to_hz from .convert import midi_to_note as midi_to_note from .convert import midi_to_svara_c as midi_to_svara_c from .convert import midi_to_svara_h as midi_to_svara_h from .convert import multi_frequency_weighting as multi_frequency_weighting from .convert import note_to_hz as note_to_hz from .convert import note_to_midi as note_to_midi from .convert import note_to_svara_c as note_to_svara_c from .convert import note_to_svara_h as note_to_svara_h from .convert import octs_to_hz as octs_to_hz from .convert import samples_like as samples_like from .convert import samples_to_frames as samples_to_frames from .convert import samples_to_time as samples_to_time from .convert import tempo_frequencies as tempo_frequencies from .convert import time_to_frames as time_to_frames from .convert import time_to_samples as time_to_samples from .convert import times_like as times_like from .convert import tuning_to_A4 as tuning_to_A4 from .harmonic import f0_harmonics as f0_harmonics from .harmonic import interp_harmonics as interp_harmonics from .harmonic import salience as salience from .intervals import interval_frequencies as interval_frequencies from .intervals import plimit_intervals as plimit_intervals from .intervals import pythagorean_intervals as pythagorean_intervals from .notation import fifths_to_note as fifths_to_note from .notation import interval_to_fjs as interval_to_fjs from .notation import key_to_degrees as key_to_degrees from .notation import key_to_notes as key_to_notes from .notation import list_mela as list_mela from .notation import list_thaat as list_thaat from .notation import mela_to_degrees as mela_to_degrees from .notation import mela_to_svara as mela_to_svara from .notation import thaat_to_degrees as thaat_to_degrees from .pitch import estimate_tuning as estimate_tuning from .pitch import piptrack as piptrack from .pitch import pitch_tuning as pitch_tuning from .pitch import pyin as pyin from .pitch import yin as yin from .spectrum import amplitude_to_db as amplitude_to_db from .spectrum import db_to_amplitude as db_to_amplitude from .spectrum import db_to_power as db_to_power from .spectrum import fmt as fmt from .spectrum import griffinlim as griffinlim from .spectrum import iirt as iirt from .spectrum import istft as istft from .spectrum import magphase as magphase from .spectrum import pcen as pcen from .spectrum import perceptual_weighting as perceptual_weighting from .spectrum import phase_vocoder as phase_vocoder from .spectrum import power_to_db as power_to_db from .spectrum import reassigned_spectrogram as reassigned_spectrogram from .spectrum import stft as stft ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2099245 librosa-1.0.0/librosa/core/audio.py0000644000175100017510000022146415236640653016672 0ustar00runnerrunner#!/usr/bin/env python # -*- coding: utf-8 -*- """Core IO, DSP and utility functions.""" from __future__ import annotations from typing import TYPE_CHECKING, overload import lazy_loader as lazy import numpy as np import scipy import soundfile as sf import soxr from numba import guvectorize, jit, stencil from .. import util from .._cache import cache from ..util.decorators import future_default from ..util.exceptions import ParameterError from ..util.files import example from .convert import frames_to_samples, time_to_samples if TYPE_CHECKING: import os from typing import Any, BinaryIO, Callable, Generator from numpy.typing import DTypeLike, NDArray from .._typing import _Array1D, _FloatLike_co, _IntLike_co, _SequenceLike # Lazy-load optional dependencies samplerate = lazy.load("samplerate") resampy = lazy.load("resampy") __all__ = [ "load", "loadx", "stream", "to_mono", "to_stereo", "to_multi", "resample", "get_duration", "get_samplerate", "autocorrelate", "lpc", "zero_crossings", "clicks", "tone", "chirp", "mu_compress", "mu_expand", ] # -- CORE ROUTINES --# # Load should never be cached, since we cannot verify that the contents of # 'path' are unchanged across calls. def load( path: str | int | os.PathLike[Any] | sf.SoundFile | BinaryIO, *, sr: float | None = 22050, mono: bool = True, offset: float = 0.0, duration: float | None = None, dtype: DTypeLike = np.float32, res_type: str = "soxr_hq", ) -> tuple[np.ndarray, int | float]: """Load an audio file as a floating point time series. Audio will be automatically resampled to the given rate (default ``sr=22050``). To preserve the native sampling rate of the file, use ``sr=None``. Parameters ---------- path : str, int, pathlib.Path, soundfile.SoundFile, or file-like object path to the input file. Any codec supported by `soundfile` will work. Any string file paths, or any object implementing Python's file interface (e.g. `pathlib.Path`) are supported as `path`. If the codec is supported by `soundfile`, then `path` can also be an open file descriptor (int) or an existing `soundfile.SoundFile` object. sr : number > 0 [scalar] target sampling rate 'None' uses the native sampling rate mono : bool convert signal to mono offset : float start reading after this time (in seconds). If negative, it will be interpreted relative to the end of the file. duration : float only load up to this much audio (in seconds) dtype : numeric type data type of ``y`` res_type : str resample type (see note) .. note:: By default, this uses `soxr`'s high-quality mode ('HQ'). For alternative resampling modes, see `resample` Returns ------- y : np.ndarray [shape=(n,) or (..., n)] audio time series. Multi-channel is supported. sr : number > 0 [scalar] sampling rate of ``y`` Examples -------- >>> # Load an ogg vorbis file >>> filename = librosa.ex('trumpet') >>> y, sr = librosa.load(filename) >>> y array([-1.407e-03, -4.461e-04, ..., -3.042e-05, 1.277e-05], dtype=float32) >>> sr 22050 >>> # Load a file and resample to 11 KHz >>> filename = librosa.ex('trumpet') >>> y, sr = librosa.load(filename, sr=11025) >>> y array([-8.746e-04, -3.363e-04, ..., -1.301e-05, 0.000e+00], dtype=float32) >>> sr 11025 >>> # Load 5 seconds of a file, starting 15 seconds in >>> filename = librosa.ex('brahms') >>> y, sr = librosa.load(filename, offset=15.0, duration=5.0) >>> y array([0.146, 0.144, ..., 0.128, 0.015], dtype=float32) >>> sr 22050 >>> # Load using an already open SoundFile object >>> import soundfile >>> sfo = soundfile.SoundFile(librosa.ex('brahms')) >>> y, sr = librosa.load(sfo) """ y, sr_native = __soundfile_load(path, offset, duration, dtype) # Final cleanup for dtype and contiguity if mono: y = to_mono(y) if sr is not None: y = resample(y, orig_sr=sr_native, target_sr=sr, res_type=res_type) else: sr = sr_native return y, sr def __soundfile_load(path, offset, duration, dtype): """Load an audio buffer using soundfile.""" if isinstance(path, sf.SoundFile): # If the user passed an existing soundfile object, # we can use it directly context = path else: # Otherwise, create the soundfile object context = sf.SoundFile(path) with context as sf_desc: sr_native = sf_desc.samplerate if offset != 0: if offset > 0: # Seek to the start of the target read sf_desc.seek(int(offset * sr_native)) else: sf_desc.seek(-int(abs(offset) * sr_native), whence=sf.SEEK_END) if duration is not None: frame_duration = int(duration * sr_native) else: frame_duration = -1 # Load the target number of frames, and transpose to match librosa form y = sf_desc.read(frames=frame_duration, dtype=dtype, always_2d=False).T return y, sr_native def _align_step_size(target_step, target_sr, orig_sr): """ When resampling a stream, we need to ensure that the target step size remains integer-valued in the original sampling rate """ if target_sr is None or target_sr == orig_sr: return target_step exact_native_step = (target_step * orig_sr) / target_sr aligned_native_step = np.round(exact_native_step) # Tolerances (rtol=1e-7, atol=1e-5) safely absorb precision errors from float arithmetic if not np.isclose(exact_native_step, aligned_native_step, rtol=1e-7, atol=1e-5): raise ParameterError( f"Target block step of {target_step} samples at {target_sr} results in a " f"fractional sample advance at original sampling rate {orig_sr}." ) return int(aligned_native_step) @future_default(param_name="sr", old_default=None, new_default=22050, version="1.1.0") def stream( path: str | int | sf.SoundFile | BinaryIO, *, block_length: int, frame_length: int, hop_length: int, sr: float | None = None, mono: bool = True, offset: float = 0.0, duration: float | None = None, fill_value: float | None = None, res_type: str = "soxr_hq", dtype: DTypeLike = np.float32, ) -> Generator[np.ndarray, None, None]: """Stream audio in fixed-length buffers. This is primarily useful for processing large files that won't fit entirely in memory at once. Instead of loading the entire audio signal into memory (as in `load`, this function produces *blocks* of audio spanning a fixed number of frames at a specified frame length and hop length. While this function strives for similar behavior to `load`, there are a few caveats that users should be aware of: 1. This function does not return audio buffers directly. It returns a generator, which you can iterate over to produce blocks of audio. A *block*, in this context, refers to a buffer of audio which spans a given number of (potentially overlapping) frames. 2. Automatic sample-rate conversion is supported, but not all combinations of ``block_length``, ``frame_length``, and ``hop_length`` will be compatible with all sampling rates. Specifically, ``(block_length * hop_length * native_sr) / sr`` must evaluate to an exact integer, where ``native_sr`` is the original sampling rate of the stream prior to resampling. 3. Many analyses require access to the entire signal to behave correctly, such as `resample`, `cqt`, or `beat_track`, so these methods will not be appropriate for streamed data. 4. The ``block_length`` parameter specifies how many frames of audio will be produced per block. Larger values will consume more memory, but will be more efficient to process down-stream. The best value will ultimately depend on your application and other system constraints. 5. By default, most librosa analyses (e.g., short-time Fourier transform) assume centered frames, which requires padding the signal at the beginning and end. This will not work correctly when the signal is carved into blocks, because it would introduce padding in the middle of the signal. To disable this feature, use ``center=False`` in all frame-based analyses. 6. If you break out of the generator loop early, the underlying audio file handle and resampling buffers will remain open until the generator object is garbage-collected. To explicitly release resources, call the generator's ``.close()`` method. See the examples below for proper usage of this function. Parameters ---------- path : str, int, sf.SoundFile, or file-like object path to the input file to stream. Any codec supported by `soundfile` is permitted here. An existing `soundfile.SoundFile` object may also be provided. block_length : int > 0 The number of frames to include in each block. Note that at the end of the file, there may not be enough data to fill an entire block, resulting in a shorter block by default. To pad the signal out so that blocks are always full length, set ``fill_value`` (see below). frame_length : int > 0 The number of samples per frame. hop_length : int > 0 The number of samples to advance between frames. Note that by when ``hop_length < frame_length``, neighboring frames will overlap. Similarly, the last frame of one *block* will overlap with the first frame of the next *block*. sr : number > 0 [scalar] target sampling rate. If not provided, the original sampling rate of the file will be used. mono : bool Convert the signal to mono during streaming offset : float Start reading after this time (in seconds) If negative, it will be interpreted relative to the end of the file. duration : float Only load up to this much audio (in seconds) fill_value : float [optional] If padding the signal to produce constant-length blocks, this value will be used at the end of the signal. In most cases, ``fill_value=0`` (silence) is expected, but you may specify any value here. res_type : str Resample type, must be one of the following: 'soxr_vhq', 'soxr_hq', 'soxr_mq' or 'soxr_lq' `soxr` Very high-, High-, Medium-, Low-quality FFT-based bandlimited interpolation. ``'soxr_hq'`` is the default setting of `soxr`. 'soxr_qq' `soxr` Quick cubic interpolation (very fast, but not bandlimited) dtype : numeric type data type of audio buffers to be produced Yields ------ y : np.ndarray An audio buffer of (at most) ``(block_length-1) * hop_length + frame_length`` samples. See Also -------- load get_samplerate soundfile.blocks Examples -------- Apply a short-term Fourier transform to blocks of 256 frames at a time. Note that streaming operation requires left-aligned frames, so we must set ``center=False`` to avoid padding artifacts. >>> filename = librosa.ex('brahms') >>> sr = librosa.get_samplerate(filename) >>> stream = librosa.stream(filename, ... block_length=256, ... frame_length=4096, ... hop_length=1024) >>> for y_block in stream: ... D_block = librosa.stft(y_block, center=False) Or compute a mel spectrogram over a stream, using a shorter frame and non-overlapping windows >>> filename = librosa.ex('brahms') >>> sr = librosa.get_samplerate(filename) >>> stream = librosa.stream(filename, ... block_length=256, ... frame_length=2048, ... hop_length=2048) >>> for y_block in stream: ... m_block = librosa.feature.melspectrogram(y=y_block, sr=sr, ... n_fft=2048, ... hop_length=2048, ... center=False) """ if not util.is_positive_int(block_length): raise ParameterError(f"block_length={block_length} must be a positive integer") if not util.is_positive_int(frame_length): raise ParameterError(f"frame_length={frame_length} must be a positive integer") if not util.is_positive_int(hop_length): raise ParameterError(f"hop_length={hop_length} must be a positive integer") if sr is not None and not (np.isfinite(sr) and sr > 0): raise ParameterError(f"sr={sr} must be a positive number") if res_type not in {"soxr_vhq", "soxr_hq", "soxr_mq", "soxr_lq", "soxr_qq"}: raise ParameterError(f"res_type={res_type} is not a valid soxr resampling mode for streaming") if isinstance(path, sf.SoundFile): sfo = path else: sfo = sf.SoundFile(path) # Get the sample rate from the file info orig_sr = sfo.samplerate # Construct the stream # Seek the soundfile object to the starting frame target_yield_size = (block_length - 1) * hop_length + frame_length target_advance = block_length * hop_length # Determine internal channel state for buffer initialization is_multichannel = sfo.channels > 1 process_channels = 1 if mono else sfo.channels needs_resampling = (sr is not None) and (orig_sr != sr) if sr is None: sr = orig_sr try: orig_read_size = _align_step_size(target_advance, sr, orig_sr) read_frames = int(duration * orig_sr) if duration is not None else -1 if needs_resampling: resampler = soxr.ResampleStream( in_rate=orig_sr, out_rate=sr, num_channels=process_channels, dtype=dtype, quality=res_type, ) capacity = target_yield_size + (target_advance * 2) buffer_shape = (capacity,) if process_channels == 1 else (capacity, process_channels) buffer = np.zeros(buffer_shape, dtype=dtype) write_idx = 0 read_idx = 0 if offset >= 0: sfo.seek(int(offset * orig_sr)) else: sfo.seek(-int(abs(offset) * orig_sr), whence=sf.SEEK_END) # Reusable buffer for the per-chunk mono downmix; the final short # chunk writes into a truncated view of it, avoiding a fresh # allocation on every block mono_buffer = ( np.empty(orig_read_size, dtype=dtype) if mono and is_multichannel else None ) for orig_chunk in sfo.blocks(blocksize=orig_read_size, overlap=0, dtype=dtype, always_2d=False, frames=read_frames): if mono and is_multichannel: # soundfile returns (samples, channels), to_mono expects (channels, samples) assert mono_buffer is not None # for mypy; set above under same condition orig_chunk = to_mono( orig_chunk.T, out=mono_buffer[: orig_chunk.shape[0]] ) if needs_resampling: target_chunk = resampler.resample_chunk(orig_chunk) else: target_chunk = orig_chunk n_incoming = target_chunk.shape[0] if write_idx + n_incoming > capacity: available = write_idx - read_idx buffer[:available] = buffer[read_idx : write_idx] read_idx = 0 write_idx = available # The following branch should never happen, but it's here # just in case something goes wrong with the input stream if write_idx + n_incoming > capacity: # pragma: no cover capacity = write_idx + n_incoming + target_yield_size new_shape = (capacity,) if process_channels == 1 else (capacity, process_channels) new_buffer = np.zeros(new_shape, dtype=dtype) new_buffer[:write_idx] = buffer[:write_idx] buffer = new_buffer buffer[write_idx : write_idx + n_incoming] = target_chunk write_idx += n_incoming while write_idx - read_idx >= target_yield_size: block = buffer[read_idx : read_idx + target_yield_size] yield block.T.copy() read_idx += target_advance if process_channels == 1: empty = np.empty((0,), dtype=buffer.dtype) else: empty = np.empty((0, process_channels), dtype=buffer.dtype) if needs_resampling: tail_chunk = resampler.resample_chunk(empty, last=True) else: tail_chunk = None final_data_list = [buffer[read_idx : write_idx]] if tail_chunk is not None and tail_chunk.shape[0] > 0: final_data_list.append(tail_chunk) remainder = np.concatenate(final_data_list, axis=0) if final_data_list else np.array([]) else: remainder = buffer[read_idx : write_idx] rem_idx = 0 while rem_idx < remainder.shape[0]: current_slice = remainder[rem_idx : rem_idx + target_yield_size] if current_slice.shape[0] < target_yield_size: pad_length = target_yield_size - current_slice.shape[0] pad_width = (0, pad_length) if process_channels == 1 else ((0, pad_length), (0, 0)) if fill_value is not None: current_slice = np.pad(current_slice, pad_width, mode="constant", constant_values=fill_value) yield current_slice.T.copy() rem_idx += target_advance finally: # If we instantiated a new SoundFile object, we should close it. If the # user passed in an existing SoundFile object, we should leave it alone. if not isinstance(path, sf.SoundFile): sfo.close() def loadx(key: str, *, hq: bool | None = None, **kwargs: Any) -> tuple[np.ndarray, int | float]: """Load an example audio file by key. This is a wrapper around `librosa.util.example` that provides the same functionality as `load`, but with the convenience of loading from the built-in examples. .. note:: This function is primarily useful for demonstration purposes, and to make documentation examples more concise. For general-purpose loading of audio files, use `load` instead. Parameters ---------- key : str The identifier for the track to load hq : bool, optional If ``True``, return the high-quality version of the recording. If ``False``, return the 22KHz mono version of the recording. If not provided, `hq` is inferred based on additional parameters in `kwargs`: - If `sr` is provided and greater than 22050 (or is `None`), then `hq` is set to `True`. - If `mono` is provided and set to `False`, then `hq` is set to `True`. - Otherwise, `hq` is set to `False`. **kwargs : additional keyword arguments Additional keyword arguments to pass to `load` Returns ------- y : np.ndarray [shape=(n,) or (..., n)] audio time series. Multi-channel is supported. sr : number > 0 [scalar] sampling rate of ``y`` See Also -------- load librosa.util.example librosa.util.list_examples Examples -------- Load the default version of the 'trumpet' example >>> y, sr = librosa.loadx('trumpet') Load the stereo version of the 'trumpet' example >>> y, sr = librosa.loadx('trumpet', mono=False) >>> # This is equivalent to the following more verbose code: >>> y, sr = librosa.load(librosa.ex('trumpet', hq=True), mono=False) """ if hq is None: if (("sr" in kwargs and (kwargs["sr"] is None or kwargs["sr"] > 22050)) or ("mono" in kwargs and kwargs["mono"] is False) ): hq = True else: hq = False try: path = example(key, hq=hq) except ParameterError as exc: raise ParameterError(f"Could not load example with key '{key}'. " "Did you mean to use librosa.load instead of loadx?") from exc return load(path, **kwargs) def _init_output( out: np.ndarray | None, shape: tuple[int, ...], dtype: np.dtype ) -> np.ndarray: """Validate a caller-provided output buffer, or allocate a new one. If ``out`` is ``None``, return a new zero-filled array of the given shape and dtype. Otherwise ``out`` must match ``shape`` exactly, and its dtype must be able to hold ``dtype`` without loss of precision (i.e. at least as precise as ``dtype``); it is zeroed in place and returned. A caller that explicitly allocates a higher-precision buffer keeps that precision. """ if out is None: return np.zeros(shape, dtype=dtype) if out.shape != shape: raise ParameterError(f"out has shape={out.shape}, but expected {shape}") if np.promote_types(out.dtype, dtype) != out.dtype: raise ParameterError( f"out has dtype={out.dtype}, which cannot hold the result dtype {dtype}" ) out[...] = 0 return out def to_mono( *signals: np.ndarray, pad: bool = True, norm: bool = True, out: np.ndarray | None = None, ) -> np.ndarray: """Convert an audio signal to mono by averaging samples across channels. Parameters ---------- *signals : np.ndarray [shape=(..., n)] One or more signals to convert to mono. Each input signal can be mono or multi-channel, and they need not all have identical lengths. pad : bool If `True`, pad the output to match the length of the longest input: `n_out = max(y.shape[-1] for y in signals)`. If `False`, trim the output to match the length of the shortest input: `n_out = min(y.shape[-1] for y in signals)`. norm : bool If `True` (default), signals are combined by averaging. If `False`, signals are combined by summing. out : np.ndarray [shape=(n_out,)] or None A pre-allocated output buffer. If provided, it must match the shape ``(n_out,)``, and its dtype must be at least as precise as the result dtype; it is zeroed and filled in place and returned, avoiding a new allocation. If ``None`` (default), a new array is allocated. Returns ------- y_mono : np.ndarray [shape=(n_out,)] All signals combined together into a single mono signal. If ``out`` was provided, this is the same object as ``out``. Notes ----- The dtype of the output signal will be the most general type that can accommodate all input signals. If the input signals have different dtypes, the output will be promoted to the most general type. .. warning:: Any input signal with more than one channel will be downmixed to mono prior to being combined with other signals. This means that the following are generally not equivalent for multi-channel inputs `y1` and `y2` when `norm=True`: >>> y_mono = librosa.to_mono(y1, y2) >>> y_mono = librosa.to_mono(np.vstack((y1, y2))) See Also -------- to_stereo to_multi util.fix_length Examples -------- Downmix a stereo input >>> y, sr = librosa.loadx('trumpet', mono=False) >>> y.shape (2, 117601) >>> y_mono = librosa.to_mono(y) >>> y_mono.shape (117601,) Mix three independent mono inputs with different lengths >>> y_440 = librosa.tone(440.0, sr=22050, duration=1.0) >>> y_550 = librosa.tone(550.0, sr=22050, duration=1.5) >>> y_660 = librosa.tone(660.0, sr=22050, duration=2.0) >>> y_mono = librosa.to_mono(y_440, y_550, y_660, pad=True) >>> y_mono.shape (44100,) """ # Validate the buffer(s) and identify lengths, dtypes, etc if not signals: raise ParameterError("At least one signal must be provided to `to_mono`.") n_min, n_max = signals[0].shape[-1], signals[0].shape[-1] dtype = signals[0].dtype for y in signals: util.valid_audio(y) n_min = min(n_min, y.shape[-1]) n_max = max(n_max, y.shape[-1]) dtype = np.promote_types(dtype, y.dtype) # Allocate and initialize the output buffer if pad: size = n_max else: size = n_min output = _init_output(out, (size,), dtype) if norm: combine = np.mean else: combine = np.sum # Now, average the signals together for y in signals: output += util.fix_length( combine(y, axis=tuple(range(y.ndim - 1))), size=output.shape[-1], axis=-1 ) if norm: # Divide by the number of input signals given output /= len(signals) return output def to_stereo( *, left: np.ndarray | None, right: np.ndarray | None, downmix: bool = True, pad: bool = True, norm: bool = True, out: np.ndarray | None = None, ) -> np.ndarray: """Combine two signals into a stereo signal. Parameters ---------- left : np.ndarray [shape=(..., n)] or None Left channel signal. Multi-channel is supported. right : np.ndarray [shape=(..., m)] or None Right channel signal. Multi-channel is supported. downmix : bool If `True`, downmix the left and right channels to mono before combining. If `False`, the left and right signals are additively combined. pad : bool If `True`, pad the shorter channel with zeros to match the length of the longer channel. If `False`, the longer channel is trimmed to match the length of the shorter channel. norm : bool If `True` (default), signals are combined by averaging. If `False`, signals are combined by summing. out : np.ndarray [shape=(2, n_out)] or None A pre-allocated output buffer. If provided, it must match the shape ``(2, n_out)``, and its dtype must be at least as precise as the result dtype; it is zeroed and filled in place and returned, avoiding a new allocation. If ``None`` (default), a new array is allocated. Returns ------- y_stereo : np.ndarray [shape=(2, n_out)] Stereo signal with left channel in the first row and right channel in the second row. If ``out`` was provided, this is the same object as ``out``. See Also -------- to_mono to_multi util.fix_length Notes ----- At least one of `left` or `right` must be provided. Examples -------- Combine two mono signals into a hard-panned stereo signal >>> y1 = librosa.tone(440.0, sr=22050, duration=1.0) >>> y2 = librosa.tone(550.0, sr=22050, duration=1.0) >>> y_stereo = librosa.to_stereo(left=y1, right=y2) Upmix a mono signal into the left or right channel in stereo >>> y_left = librosa.to_stereo(left=y1, right=None) >>> y_right = librosa.to_stereo(left=None, right=y2) Downmix a stereo signal into the left channel, and mix a third mono signal into the right channel >>> y3 = librosa.tone(660.0, sr=22050, duration=1.0) >>> y_mix = librosa.to_stereo(left=y_stereo, right=y3, downmix=True) Keep the original stereo signal separated, but mix a third signal into the right channel: >>> y_mix = librosa.to_stereo(left=y_stereo, right=y3, downmix=False) """ # This flag tracks whether we had only one signal to begin with onesided = True if left is None and right is None: raise ParameterError("At least one of 'left' or 'right' must be provided") elif left is None: # These are somewhat inefficient ways to allocate a silent channel, # but it makes the logic simple and clear below left = np.zeros_like(right) elif right is None: right = np.zeros_like(left) else: onesided = False # This assert tells mypy that both arrays now exist assert left is not None and right is not None # First, deal with padding if pad: size = max(left.shape[-1], right.shape[-1]) else: size = min(left.shape[-1], right.shape[-1]) # This implements either padding or trimming left = util.fix_length(left, size=size, axis=-1) right = util.fix_length(right, size=size, axis=-1) # Get a compatible dtype for both channels dtype = np.promote_types(left.dtype, right.dtype) left = left.astype(dtype, copy=False) right = right.astype(dtype, copy=False) # Create an empty stereo output buffer output = _init_output(out, (2, size), dtype) if downmix: # Downmix directly into the output rows to avoid a second allocation to_mono(left, norm=norm, out=output[0]) to_mono(right, norm=norm, out=output[1]) else: if left.ndim == 1: output[0] = left elif left.ndim == 2 and left.shape[0] == 2: output[:] = left else: raise ParameterError( f"left input has unsupported shape {left.shape} for downmix=False" ) if right.ndim == 1: output[1] += right elif right.ndim == 2 and right.shape[0] == 2: output[:] += right else: raise ParameterError( f"right input has unsupported shape {right.shape} for downmix=False" ) if norm and not onesided: # Only normalize here if we had both channels on input output /= 2 return output def to_multi( *signals: np.ndarray, downmix: bool = True, pad: bool = True, norm: bool = True, out: np.ndarray | None = None, ) -> np.ndarray: """Combine multiple signals into a multi-channel signal. Parameters ---------- *signals : np.ndarray [shape=(..., n)] One or more signals to combine into a multi-channel signal. Each input signal can be mono or multi-channel, and they need not all have identical lengths. downmix : bool If `True`, downmix each input signal to mono before combining. If `False`, the input signals are additively combined, and all must have identical channel layouts (shape excluding the trailing length dimension). pad : bool If `True`, pad the output to match the length of the longest input signal. If `False`, trim the output to match the length of the shortest input signal. norm : bool If `True` (default), signals are combined by averaging. If `False`, signals are combined by summing. out : np.ndarray [shape=(m, n_out) or shape=(..., n_out)] or None A pre-allocated output buffer. If provided, it must match the result shape, and its dtype must be at least as precise as the result dtype; it is zeroed and filled in place and returned, avoiding a new allocation. If ``None`` (default), a new array is allocated. Returns ------- y_multi : np.ndarray [shape=(m, n_out) or shape=(..., n_out)] Multi-channel combination of the input signals, where `m` corresponds to the number of signals (if `downmix=True`), and `n_out` is the length of the output signal determined by the padding mode. If ``out`` was provided, this is the same object as ``out``. See Also -------- to_mono to_stereo util.fix_length Examples -------- Combine three mono signals of different lengths into a multi-channel signal >>> y1 = librosa.tone(440.0, sr=22050, duration=1.0) >>> y2 = librosa.tone(550.0, sr=22050, duration=1.0) >>> y3 = librosa.tone(660.0, sr=22050, duration=2.0) >>> y_multi = librosa.to_multi(y1, y2, y3, pad=True) >>> y_multi.shape (3, 44100) """ # Validate the buffer(s) and identify lengths, dtypes, etc if not signals: raise ParameterError("At least one signal must be provided.") n_min, n_max = signals[0].shape[-1], signals[0].shape[-1] dtype = signals[0].dtype if downmix: # If all channels are downmixed to mono, # then the layout will be (n_signals, n_out) channel_layout = tuple((len(signals),)) else: # If we're not downmixing, then all have to have # identical channel layout channel_layout = tuple(signals[0].shape[:-1]) for y in signals: util.valid_audio(y) n_min = min(n_min, y.shape[-1]) n_max = max(n_max, y.shape[-1]) dtype = np.promote_types(dtype, y.dtype) if not downmix and y.shape[:-1] != channel_layout: raise ParameterError( f"Cannot combine signals with different channel layouts {y.shape[:-1]} when downmix=False" ) # Allocate and initialize the output buffer if pad: size = n_max else: size = n_min output = _init_output(out, (*channel_layout, size), dtype) if downmix: # Downmix each signal to mono and mix into the output for i, y in enumerate(signals): y = y.astype(dtype, copy=False) if y.shape[-1] == size: # No length adjustment needed: downmix straight into the row to_mono(y, norm=norm, out=output[i]) else: # Length differs, so fix_length must allocate; a pass-through # of out= is not possible here output[i] = util.fix_length(to_mono(y, norm=norm), size=size, axis=-1) else: # Truncate and mix into the output for y in signals: output += util.fix_length(y, size=size, axis=-1) if norm: # Divide by the number of input signals given output /= len(signals) return output @cache(level=20) def resample( y: np.ndarray, *, orig_sr: float, target_sr: float, res_type: str = "soxr_hq", fix: bool = True, scale: bool = False, axis: int = -1, **kwargs: Any, ) -> np.ndarray: """Resample a time series from orig_sr to target_sr By default, this uses a high-quality method (`soxr_hq`) for band-limited sinc interpolation. The alternate ``res_type`` values listed below offer different trade-offs of speed and quality. Parameters ---------- y : np.ndarray [shape=(..., n, ...)] audio time series, with `n` samples along the specified axis. orig_sr : number > 0 [scalar] original sampling rate of ``y`` target_sr : number > 0 [scalar] target sampling rate res_type : str (default: `soxr_hq`) resample type 'soxr_vhq', 'soxr_hq', 'soxr_mq' or 'soxr_lq' `soxr` Very high-, High-, Medium-, Low-quality FFT-based bandlimited interpolation. ``'soxr_hq'`` is the default setting of `soxr`. 'soxr_qq' `soxr` Quick cubic interpolation (very fast, but not bandlimited) 'kaiser_best' `resampy` high-quality mode 'kaiser_fast' `resampy` faster method 'fft' or 'scipy' `scipy.signal.resample` Fourier method. 'polyphase' `scipy.signal.resample_poly` polyphase filtering. (fast) 'linear' `samplerate` linear interpolation. (very fast, but not bandlimited) 'zero_order_hold' `samplerate` repeat the last value between samples. (very fast, but not bandlimited) 'sinc_best', 'sinc_medium' or 'sinc_fastest' `samplerate` high-, medium-, and low-quality bandlimited sinc interpolation. .. note:: Not all options yield a bandlimited interpolator. If you use `soxr_qq`, `polyphase`, `linear`, or `zero_order_hold`, you need to be aware of possible aliasing effects. .. note:: `samplerate` and `resampy` are not installed with `librosa`. To use `samplerate` or `resampy`, they should be installed manually:: $ pip install samplerate $ pip install resampy .. note:: When using ``res_type='polyphase'``, only integer sampling rates are supported. fix : bool adjust the length of the resampled signal to be of size exactly ``ceil(target_sr * len(y) / orig_sr)`` scale : bool Scale the resampled signal so that ``y`` and ``y_hat`` have approximately equal total energy. axis : int The target axis along which to resample. Defaults to the trailing axis. **kwargs : additional keyword arguments If ``fix==True``, additional keyword arguments to pass to `librosa.util.fix_length`. Returns ------- y_hat : np.ndarray [shape=(..., n * target_sr / orig_sr, ...)] ``y`` resampled from ``orig_sr`` to ``target_sr`` along the target axis Raises ------ ParameterError If ``res_type='polyphase'`` and ``orig_sr`` or ``target_sr`` are not both integer-valued. See Also -------- librosa.util.fix_length scipy.signal.resample resampy samplerate.converters.resample soxr.resample Notes ----- This function caches at level 20. Examples -------- Downsample from 22 KHz to 8 KHz >>> y, sr = librosa.loadx('trumpet', sr=22050) >>> y_8k = librosa.resample(y, orig_sr=sr, target_sr=8000) >>> y.shape, y_8k.shape ((117601,), (42668,)) """ # First, validate the audio buffer util.valid_audio(y) if orig_sr == target_sr: return y ratio = float(target_sr) / orig_sr n_samples = int(np.ceil(y.shape[axis] * ratio)) if res_type in ("scipy", "fft"): import scipy.signal y_hat = scipy.signal.resample(y, n_samples, axis=axis) elif res_type == "polyphase": if int(orig_sr) != orig_sr or int(target_sr) != target_sr: raise ParameterError( "polyphase resampling is only supported for integer-valued sampling rates." ) import scipy.signal # For polyphase resampling, we need up- and down-sampling ratios # We can get those from the greatest common divisor of the rates # as long as the rates are integrable orig_sr = int(orig_sr) target_sr = int(target_sr) gcd = np.gcd(orig_sr, target_sr) y_hat = scipy.signal.resample_poly( y, target_sr // gcd, orig_sr // gcd, axis=axis ) elif res_type in ( "linear", "zero_order_hold", "sinc_best", "sinc_fastest", "sinc_medium", ): # Use numpy to vectorize the resampler along the target axis # This is because samplerate does not support ndim>2 generally. y_hat = np.apply_along_axis( samplerate.resample, axis=axis, arr=y, ratio=ratio, converter_type=res_type ) elif res_type.startswith("soxr"): # Use numpy to vectorize the resampler along the target axis # This is because soxr does not support ndim>2 generally. y_hat = np.apply_along_axis( soxr.resample, axis=axis, arr=y, in_rate=orig_sr, out_rate=target_sr, quality=res_type, ) else: y_hat = resampy.resample(y, orig_sr, target_sr, filter=res_type, axis=axis) if fix: y_hat = util.fix_length(y_hat, size=n_samples, axis=axis, **kwargs) if scale: y_hat /= np.sqrt(ratio) # Match dtypes return np.asarray(y_hat, dtype=y.dtype) def get_duration( *, y: np.ndarray | None = None, sr: float = 22050, S: np.ndarray | None = None, n_fft: int = 2048, hop_length: int = 512, center: bool = True, path: str | os.PathLike[Any] | None = None, ) -> float: """Compute the duration (in seconds) of an audio time series, feature matrix, or filename. Examples -------- >>> # Load an example audio file >>> y, sr = librosa.loadx('trumpet') >>> librosa.get_duration(y=y, sr=sr) 5.333378684807256 >>> # Or directly from an audio file >>> librosa.get_duration(filename=librosa.ex('trumpet')) 5.333378684807256 >>> # Or compute duration from an STFT matrix >>> y, sr = librosa.loadx('trumpet') >>> S = librosa.stft(y) >>> librosa.get_duration(S=S, sr=sr) 5.317369614512471 >>> # Or a non-centered STFT matrix >>> S_left = librosa.stft(y, center=False) >>> librosa.get_duration(S=S_left, sr=sr) 5.224489795918367 Parameters ---------- y : np.ndarray [shape=(..., n)] or None audio time series. Multi-channel is supported. sr : number > 0 [scalar] audio sampling rate of ``y`` S : np.ndarray [shape=(..., d, t)] or None STFT matrix, or any STFT-derived matrix (e.g., chromagram or mel spectrogram). Durations calculated from spectrogram inputs are only accurate up to the frame resolution. If high precision is required, it is better to use the audio time series directly. n_fft : int > 0 [scalar] length of the FFT frame used when computing ``S`` hop_length : int > 0 [ scalar] number of audio samples between columns of ``S`` center : bool - If ``True``, ``S[:, t]`` is centered at ``y[t * hop_length]`` - If ``False``, then ``S[:, t]`` begins at ``y[t * hop_length]`` path : str, path, or file-like If provided, all other parameters are ignored, and the duration is calculated directly from the audio file. Note that this avoids loading the contents into memory, and is therefore useful for querying the duration of long files. As in ``load``, this can also be an integer or open file-handle that can be processed by ``soundfile``. Returns ------- d : float >= 0 Duration (in seconds) of the input time series or spectrogram. Raises ------ ParameterError if none of ``y``, ``S``, or ``path`` are provided. Notes ----- `get_duration` can be applied to a file (``path``), a spectrogram (``S``), or audio buffer (``y, sr``). Only one of these three options should be provided. If you do provide multiple options (e.g., ``path`` and ``S``), then ``path`` takes precedence over ``S``, and ``S`` takes precedence over ``(y, sr)``. """ if path is not None: return sf.info(path).duration # type: ignore if y is None: if S is None: raise ParameterError("At least one of (y, sr), S, or path must be provided") n_frames = S.shape[-1] n_samples = n_fft + hop_length * (n_frames - 1) # If centered, we lose half a window from each end of S if center: n_samples = n_samples - 2 * int(n_fft // 2) else: n_samples = y.shape[-1] return float(n_samples) / sr def get_samplerate(path: str | int | sf.SoundFile | BinaryIO) -> float: """Get the sampling rate for a given file. Parameters ---------- path : str, int, soundfile.SoundFile, or file-like The path to the file to be loaded As in ``load``, this can also be an integer or open file-handle that can be processed by `soundfile`. An existing `soundfile.SoundFile` object can also be supplied. Returns ------- sr : number > 0 The sampling rate of the given audio file Examples -------- Get the sampling rate for the included audio file >>> path = librosa.ex('trumpet') >>> librosa.get_samplerate(path) 22050 """ if isinstance(path, sf.SoundFile): return path.samplerate # type: ignore return sf.info(path).samplerate # type: ignore @cache(level=20) def autocorrelate( y: np.ndarray, *, max_size: int | None = None, axis: int = -1 ) -> np.ndarray: """Bounded-lag auto-correlation Parameters ---------- y : np.ndarray array to autocorrelate max_size : int > 0 or None maximum correlation lag. If unspecified, defaults to ``y.shape[axis]`` (unbounded) axis : int The axis along which to autocorrelate. By default, the last axis (-1) is taken. Returns ------- z : np.ndarray truncated autocorrelation ``y*y`` along the specified axis. If ``max_size`` is specified, then ``z.shape[axis]`` is bounded to ``max_size``. Notes ----- This function caches at level 20. Examples -------- Compute full autocorrelation of ``y`` >>> y, sr = librosa.loadx('trumpet') >>> librosa.autocorrelate(y) array([ 6.899e+02, 6.236e+02, ..., 3.710e-08, -1.796e-08]) Compute onset strength auto-correlation up to 4 seconds >>> import matplotlib.pyplot as plt >>> odf = librosa.onset.onset_strength(y=y, sr=sr, hop_length=512) >>> ac = librosa.autocorrelate(odf, max_size=4 * sr // 512) >>> fig, ax = plt.subplots() >>> ax.plot(ac) >>> ax.set(title='Auto-correlation', xlabel='Lag (frames)') """ if max_size is None: max_size = y.shape[axis] max_size = int(min(max_size, y.shape[axis])) real = not np.iscomplexobj(y) # Pad out the signal to support full-length auto-correlation n_pad = scipy.fft.next_fast_len(2 * y.shape[axis] - 1, real=real) autocorr: np.ndarray if real: # Compute the power spectrum along the chosen axis powspec = util.abs2(scipy.fft.rfft(y, n=n_pad, axis=axis)) # Convert back to time domain autocorr = scipy.fft.irfft(powspec, n=n_pad, axis=axis) else: # Compute the power spectrum along the chosen axis powspec = util.abs2(scipy.fft.fft(y, n=n_pad, axis=axis)) # Convert back to time domain autocorr = scipy.fft.ifft(powspec, n=n_pad, axis=axis) # Slice down to max_size subslice = [slice(None)] * autocorr.ndim subslice[axis] = slice(max_size) autocorr_slice: np.ndarray = autocorr[tuple(subslice)] return autocorr_slice def lpc(y: np.ndarray, *, order: int, axis: int = -1) -> np.ndarray: """Linear Prediction Coefficients via Burg's method This function applies Burg's method to estimate coefficients of a linear filter on ``y`` of order ``order``. Burg's method is an extension to the Yule-Walker approach, which are both sometimes referred to as LPC parameter estimation by autocorrelation. It follows the description and implementation approach described in the introduction by Marple. [#]_ N.B. This paper describes a different method, which is not implemented here, but has been chosen for its clear explanation of Burg's technique in its introduction. .. [#] Larry Marple. A New Autoregressive Spectrum Analysis Algorithm. IEEE Transactions on Acoustics, Speech, and Signal Processing vol 28, no. 4, 1980. Parameters ---------- y : np.ndarray [shape=(..., n)] Time series to fit. Multi-channel is supported.. order : int > 0 Order of the linear filter axis : int Axis along which to compute the coefficients Returns ------- a : np.ndarray [shape=(..., order + 1)] LP prediction error coefficients, i.e. filter denominator polynomial. Note that the length along the specified ``axis`` will be ``order+1``. Raises ------ ParameterError - If ``y`` is not valid audio as per `librosa.util.valid_audio` - If ``order < 1`` or not integer FloatingPointError - If ``y`` is ill-conditioned See Also -------- scipy.signal.lfilter Examples -------- Compute LP coefficients of y at order 16 on entire series >>> y, sr = librosa.loadx('libri1') >>> librosa.lpc(y, order=16) Compute LP coefficients, and plot LP estimate of original series >>> import matplotlib.pyplot as plt >>> import scipy >>> y, sr = librosa.loadx('libri1', duration=0.020) >>> a = librosa.lpc(y, order=2) >>> b = np.hstack([[0], -1 * a[1:]]) >>> y_hat = scipy.signal.lfilter(b, [1], y) >>> fig, ax = plt.subplots() >>> ax.plot(y) >>> ax.plot(y_hat, linestyle='--') >>> ax.legend(['y', 'y_hat']) >>> ax.set_title('LP Model Forward Prediction') """ if not util.is_positive_int(order): raise ParameterError(f"order={order} must be an integer > 0") util.valid_audio(y) # Move the lpc axis around front, because numba is silly y = y.swapaxes(axis, 0) dtype = y.dtype shape = list(y.shape) shape[0] = order + 1 ar_coeffs = np.zeros(tuple(shape), dtype=dtype) ar_coeffs[0] = 1 ar_coeffs_prev = ar_coeffs.copy() shape[0] = 1 reflect_coeff = np.zeros(shape, dtype=dtype) den = reflect_coeff.copy() epsilon = util.tiny(den) # Call the helper, and swap the results back to the target axis position return np.swapaxes( __lpc(y, order, ar_coeffs, ar_coeffs_prev, reflect_coeff, den, epsilon), 0, axis ) @jit(nopython=True, cache=True) # type: ignore def __lpc( y: np.ndarray, order: int, ar_coeffs: np.ndarray, ar_coeffs_prev: np.ndarray, reflect_coeff: np.ndarray, den: np.ndarray, epsilon: float, ) -> np.ndarray: # This implementation follows the description of Burg's algorithm given in # section III of Marple's paper referenced in the docstring. # # We use the Levinson-Durbin recursion to compute AR coefficients for each # increasing model order by using those from the last. We maintain two # arrays and then flip them each time we increase the model order so that # we may use all the coefficients from the previous order while we compute # those for the new one. These two arrays hold ar_coeffs for order M and # order M-1. (Corresponding to a_{M,k} and a_{M-1,k} in eqn 5) # These two arrays hold the forward and backward prediction error. They # correspond to f_{M-1,k} and b_{M-1,k} in eqns 10, 11, 13 and 14 of # Marple. First they are used to compute the reflection coefficient at # order M from M-1 then are used as f_{M,k} and b_{M,k} for each # iteration of the below loop fwd_pred_error = y[1:] bwd_pred_error = y[:-1] # DEN_{M} from eqn 16 of Marple. den[0] = np.sum(fwd_pred_error**2 + bwd_pred_error**2, axis=0) for i in range(order): # can be removed if we keep the epsilon bias # if np.any(den <= 0): # raise FloatingPointError("numerical error, input ill-conditioned?") # Eqn 15 of Marple, with fwd_pred_error and bwd_pred_error # corresponding to f_{M-1,k+1} and b{M-1,k} and the result as a_{M,M} reflect_coeff[0] = np.sum(bwd_pred_error * fwd_pred_error, axis=0) reflect_coeff[0] *= -2 reflect_coeff[0] /= den[0] + epsilon # Now we use the reflection coefficient and the AR coefficients from # the last model order to compute all of the AR coefficients for the # current one. This is the Levinson-Durbin recursion described in # eqn 5. # Note 1: We don't have to care about complex conjugates as our signals # are all real-valued # Note 2: j counts 1..order+1, i-j+1 counts order..0 # Note 3: The first element of ar_coeffs* is always 1, which copies in # the reflection coefficient at the end of the new AR coefficient array # after the preceding coefficients ar_coeffs_prev, ar_coeffs = ar_coeffs, ar_coeffs_prev for j in range(1, i + 2): # reflection multiply should be broadcast ar_coeffs[j] = ( ar_coeffs_prev[j] + reflect_coeff[0] * ar_coeffs_prev[i - j + 1] ) # Update the forward and backward prediction errors corresponding to # eqns 13 and 14. We start with f_{M-1,k+1} and b_{M-1,k} and use them # to compute f_{M,k} and b_{M,k} fwd_pred_error_tmp = fwd_pred_error fwd_pred_error = fwd_pred_error + reflect_coeff * bwd_pred_error bwd_pred_error = bwd_pred_error + reflect_coeff * fwd_pred_error_tmp # SNIP - we are now done with order M and advance. M-1 <- M # Compute DEN_{M} using the recursion from eqn 17. # # reflect_coeff = a_{M-1,M-1} (we have advanced M) # den = DEN_{M-1} (rhs) # bwd_pred_error = b_{M-1,N-M+1} (we have advanced M) # fwd_pred_error = f_{M-1,k} (we have advanced M) # den <- DEN_{M} (lhs) # q = 1.0 - reflect_coeff[0] ** 2 den[0] = q * den[0] - bwd_pred_error[-1] ** 2 - fwd_pred_error[0] ** 2 # Shift up forward error. # # fwd_pred_error <- f_{M-1,k+1} # bwd_pred_error <- b_{M-1,k} # # N.B. We do this after computing the denominator using eqn 17 but # before using it in the numerator in eqn 15. fwd_pred_error = fwd_pred_error[1:] bwd_pred_error = bwd_pred_error[:-1] return ar_coeffs @stencil # type: ignore def _zc_stencil(x: np.ndarray, threshold: float, zero_pos: bool) -> NDArray[np.bool]: """Stencil to compute zero crossings""" x0 = x[0] if -threshold <= x0 <= threshold: x0 = 0 x1 = x[-1] if -threshold <= x1 <= threshold: x1 = 0 if zero_pos: return np.signbit(x0) != np.signbit(x1) # type: ignore else: return np.sign(x0) != np.sign(x1) # type: ignore @guvectorize( "(n),(),()->(n)", cache=True, nopython=True, ) # type: ignore def _zc_wrapper( x: np.ndarray, threshold: float, zero_pos: bool, y: NDArray[np.bool], ) -> None: # pragma: no cover """Vectorized wrapper for zero crossing stencil""" y[:] = _zc_stencil(x, threshold, zero_pos) @cache(level=20) def zero_crossings( y: np.ndarray, *, threshold: float = 1e-10, ref_magnitude: float | Callable | None = None, pad: bool = True, zero_pos: bool = True, axis: int = -1, ) -> NDArray[np.bool]: """Find the zero-crossings of a signal ``y``: indices ``i`` such that ``sign(y[i]) != sign(y[j])``. If ``y`` is multi-dimensional, then zero-crossings are computed along the specified ``axis``. Parameters ---------- y : np.ndarray The input array threshold : float >= 0 If non-zero, values where ``-threshold <= y <= threshold`` are clipped to 0. ref_magnitude : float > 0 or callable If numeric, the threshold is scaled relative to ``ref_magnitude``. If callable, the threshold is scaled relative to ``ref_magnitude(np.abs(y))``. pad : bool If ``True``, then ``y[0]`` is considered a valid zero-crossing. zero_pos : bool If ``True`` then the value 0 is interpreted as having positive sign. If ``False``, then 0, -1, and +1 all have distinct signs. axis : int Axis along which to compute zero-crossings. Returns ------- zero_crossings : np.ndarray [shape=y.shape, dtype=bool] Indicator array of zero-crossings in ``y`` along the selected axis. Notes ----- This function caches at level 20. Examples -------- >>> # Generate a time-series >>> y = np.sin(np.linspace(0, 4 * 2 * np.pi, 20)) >>> y array([ 0.000e+00, 9.694e-01, 4.759e-01, -7.357e-01, -8.372e-01, 3.247e-01, 9.966e-01, 1.646e-01, -9.158e-01, -6.142e-01, 6.142e-01, 9.158e-01, -1.646e-01, -9.966e-01, -3.247e-01, 8.372e-01, 7.357e-01, -4.759e-01, -9.694e-01, -9.797e-16]) >>> # Compute zero-crossings >>> z = librosa.zero_crossings(y) >>> z array([ True, False, False, True, False, True, False, False, True, False, True, False, True, False, False, True, False, True, False, True], dtype=bool) >>> # Stack y against the zero-crossing indicator >>> librosa.util.stack([y, z], axis=-1) array([[ 0.000e+00, 1.000e+00], [ 9.694e-01, 0.000e+00], [ 4.759e-01, 0.000e+00], [ -7.357e-01, 1.000e+00], [ -8.372e-01, 0.000e+00], [ 3.247e-01, 1.000e+00], [ 9.966e-01, 0.000e+00], [ 1.646e-01, 0.000e+00], [ -9.158e-01, 1.000e+00], [ -6.142e-01, 0.000e+00], [ 6.142e-01, 1.000e+00], [ 9.158e-01, 0.000e+00], [ -1.646e-01, 1.000e+00], [ -9.966e-01, 0.000e+00], [ -3.247e-01, 0.000e+00], [ 8.372e-01, 1.000e+00], [ 7.357e-01, 0.000e+00], [ -4.759e-01, 1.000e+00], [ -9.694e-01, 0.000e+00], [ -9.797e-16, 1.000e+00]]) >>> # Find the indices of zero-crossings >>> np.nonzero(z) (array([ 0, 3, 5, 8, 10, 12, 15, 17, 19]),) """ if callable(ref_magnitude): threshold = threshold * ref_magnitude(np.abs(y)) elif ref_magnitude is not None: threshold = threshold * ref_magnitude yi = y.swapaxes(-1, axis) z = np.empty_like(y, dtype=bool) zi = z.swapaxes(-1, axis) _zc_wrapper(yi, threshold, zero_pos, zi) zi[..., 0] = pad return z def clicks( *, times: _SequenceLike[_FloatLike_co] | None = None, frames: _SequenceLike[_IntLike_co] | None = None, sr: float = 22050, hop_length: int = 512, click_freq: float = 1000.0, click_duration: float = 0.1, click: np.ndarray | None = None, length: int | None = None, ) -> NDArray[np.float32]: """Construct a "click track". This returns a signal with the signal ``click`` sound placed at each specified time. Parameters ---------- times : np.ndarray or None times to place clicks, in seconds frames : np.ndarray or None frame indices to place clicks sr : number > 0 desired sampling rate of the output signal hop_length : int > 0 if positions are specified by ``frames``, the number of samples between frames. click_freq : float > 0 frequency (in Hz) of the default click signal. Default is 1KHz. click_duration : float > 0 duration (in seconds) of the default click signal. Default is 100ms. click : np.ndarray or None (optional) click signal sample to use instead of the default click. Multi-channel is supported. length : int > 0 desired number of samples in the output signal Returns ------- click_signal : np.ndarray Synthesized click signal. This will be monophonic by default, or match the number of channels to a provided ``click`` signal. Raises ------ ParameterError - If neither ``times`` nor ``frames`` are provided. - If any of ``click_freq``, ``click_duration``, or ``length`` are out of range. Examples -------- >>> # Sonify detected beat events >>> y, sr = librosa.loadx('choice', duration=10) >>> tempo, beats = librosa.beat.beat_track(y=y, sr=sr) >>> y_beats = librosa.clicks(frames=beats, sr=sr) >>> # Or generate a signal of the same length as y >>> y_beats = librosa.clicks(frames=beats, sr=sr, length=len(y)) >>> # Or use timing instead of frame indices >>> times = librosa.frames_to_time(beats, sr=sr) >>> y_beat_times = librosa.clicks(times=times, sr=sr) >>> # Or with a click frequency of 880Hz and a 500ms sample >>> y_beat_times880 = librosa.clicks(times=times, sr=sr, ... click_freq=880, click_duration=0.5) Display click waveform next to the spectrogram >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=2, sharex=True) >>> S = librosa.feature.melspectrogram(y=y, sr=sr) >>> librosa.display.specshow(S, vscale='dBFS[power]', ... x_axis='time', y_axis='mel', ax=ax[0]) >>> librosa.display.waveshow(y_beat_times, sr=sr, label='Beat clicks', ... ax=ax[1]) >>> ax[1].legend() >>> ax[0].label_outer() >>> ax[0].set_title(None) """ # Compute sample positions from time or frames positions: np.ndarray if times is None: if frames is None: raise ParameterError('either "times" or "frames" must be provided') positions = frames_to_samples(frames, hop_length=hop_length) else: # Convert times to positions positions = time_to_samples(times, sr=sr) if click is not None: # Check that we have a well-formed audio buffer util.valid_audio(click) else: # Create default click signal if click_duration <= 0: raise ParameterError("click_duration must be strictly positive") if click_freq <= 0: raise ParameterError("click_freq must be strictly positive") angular_freq = 2 * np.pi * click_freq / float(sr) click = np.logspace(0, -10, num=int(sr * click_duration), base=2.0) click *= np.sin(angular_freq * np.arange(len(click))) # Set default length if length is None: length = positions.max() + click.shape[-1] else: if length < 1: raise ParameterError("length must be a positive integer") # Filter out any positions past the length boundary positions = positions[positions < length] # Pre-allocate click signal shape = list(click.shape) shape[-1] = length click_signal = np.zeros(shape, dtype=np.float32) # Place clicks for start in positions: # Compute the end-point of this click end = start + click.shape[-1] if end >= length: click_signal[..., start:] += click[..., : length - start] else: # Normally, just add a click here click_signal[..., start:end] += click return click_signal def tone( frequency: _FloatLike_co, *, sr: float = 22050, length: int | None = None, duration: float | None = None, phi: float | None = None, ) -> _Array1D[np.float64]: """Construct a pure tone (cosine) signal at a given frequency. Parameters ---------- frequency : float > 0 frequency sr : number > 0 desired sampling rate of the output signal length : int > 0 desired number of samples in the output signal. When both ``duration`` and ``length`` are defined, ``length`` takes priority. duration : float > 0 desired duration in seconds. When both ``duration`` and ``length`` are defined, ``length`` takes priority. phi : float or None phase offset, in radians. If unspecified, defaults to ``-np.pi * 0.5``. Returns ------- tone_signal : np.ndarray [shape=(length,), dtype=float64] Synthesized pure sine tone signal Raises ------ ParameterError - If ``frequency`` is not provided. - If neither ``length`` nor ``duration`` are provided. Examples -------- Generate a pure sine tone A4 >>> tone = librosa.tone(440, duration=1, sr=22050) Or generate the same signal using `length` >>> tone = librosa.tone(440, sr=22050, length=22050) Display spectrogram >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots() >>> S = librosa.feature.melspectrogram(y=tone) >>> librosa.display.specshow(S, vscale='dBFS[power]', ... x_axis='time', y_axis='mel', ax=ax) """ if frequency is None: raise ParameterError('"frequency" must be provided') # Compute signal length if length is None: if duration is None: raise ParameterError('either "length" or "duration" must be provided') length = int(duration * sr) if phi is None: phi = -np.pi * 0.5 y: np.ndarray = np.cos(2 * np.pi * frequency * np.arange(length) / sr + phi) return y def chirp( *, fmin: _FloatLike_co, fmax: _FloatLike_co, sr: float = 22050, length: int | None = None, duration: float | None = None, linear: bool = False, phi: float | None = None, ) -> _Array1D[np.float64]: """Construct a "chirp" or "sine-sweep" signal. The chirp sweeps from frequency ``fmin`` to ``fmax`` (in Hz). Parameters ---------- fmin : float > 0 initial frequency fmax : float > 0 final frequency sr : number > 0 desired sampling rate of the output signal length : int > 0 desired number of samples in the output signal. When both ``duration`` and ``length`` are defined, ``length`` takes priority. duration : float > 0 desired duration in seconds. When both ``duration`` and ``length`` are defined, ``length`` takes priority. linear : bool - If ``True``, use a linear sweep, i.e., frequency changes linearly with time - If ``False``, use a exponential sweep. Default is ``False``. phi : float or None phase offset, in radians. If unspecified, defaults to ``-np.pi * 0.5``. Returns ------- chirp_signal : np.ndarray [shape=(length,), dtype=float64] Synthesized chirp signal Raises ------ ParameterError - If either ``fmin`` or ``fmax`` are not provided. - If neither ``length`` nor ``duration`` are provided. See Also -------- scipy.signal.chirp Examples -------- Generate a exponential chirp from A2 to A8 >>> exponential_chirp = librosa.chirp(fmin=110, fmax=110*64, duration=1, sr=22050) Or generate the same signal using ``length`` >>> exponential_chirp = librosa.chirp(fmin=110, fmax=110*64, sr=22050, length=22050) Or generate a linear chirp instead >>> linear_chirp = librosa.chirp(fmin=110, fmax=110*64, duration=1, linear=True, sr=22050) Display spectrogram for both exponential and linear chirps. >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=2, sharex=True, sharey=True) >>> S_exponential = np.abs(librosa.stft(y=exponential_chirp)) >>> librosa.display.specshow(S_exponential, vscale='dBFS', ... x_axis='time', y_axis='linear', ax=ax[0]) >>> ax[0].set(title='Exponential chirp', xlabel=None) >>> ax[0].label_outer() >>> S_linear = np.abs(librosa.stft(y=linear_chirp)) >>> librosa.display.specshow(S_linear, vscale='dBFS', ... x_axis='time', y_axis='linear', ax=ax[1]) >>> ax[1].set(title='Linear chirp') """ if fmin is None or fmax is None: raise ParameterError('both "fmin" and "fmax" must be provided') # Compute signal duration period = 1.0 / sr if length is None: if duration is None: raise ParameterError('either "length" or "duration" must be provided') else: duration = period * length if phi is None: phi = -np.pi * 0.5 method = "linear" if linear else "logarithmic" import scipy.signal y: np.ndarray = scipy.signal.chirp( # type: ignore[call-overload] np.arange(int(duration * sr)) / sr, fmin, duration, fmax, method=method, phi=phi / np.pi * 180, # scipy.signal.chirp uses degrees for phase offset ) return y def mu_compress( x: np.ndarray | _FloatLike_co, *, mu: float = 255, quantize: bool = True ) -> np.ndarray: """Compression by μ-law Given an input signal ``-1 <= x <= 1``, the mu-law compression is calculated by:: sign(x) * ln(1 + mu * abs(x)) / ln(1 + mu) Parameters ---------- x : np.ndarray with values in [-1, +1] The input signal to compress mu : positive number The compression parameter. Values of the form ``2**n - 1`` (e.g., 15, 31, 63, etc.) are most common. quantize : bool If ``True``, quantize the compressed values into ``1 + mu`` distinct integer values. If ``False``, mu-law compression is applied without quantization. Returns ------- x_compressed : np.ndarray The compressed signal. Raises ------ ParameterError If ``x`` has values outside the range [-1, +1] If ``mu <= 0`` See Also -------- mu_expand Examples -------- Compression without quantization >>> x = np.linspace(-1, 1, num=16) >>> x array([-1. , -0.86666667, -0.73333333, -0.6 , -0.46666667, -0.33333333, -0.2 , -0.06666667, 0.06666667, 0.2 , 0.33333333, 0.46666667, 0.6 , 0.73333333, 0.86666667, 1. ]) >>> y = librosa.mu_compress(x, quantize=False) >>> y array([-1. , -0.97430198, -0.94432361, -0.90834832, -0.86336132, -0.80328309, -0.71255496, -0.52124063, 0.52124063, 0.71255496, 0.80328309, 0.86336132, 0.90834832, 0.94432361, 0.97430198, 1. ]) Compression with quantization >>> y = librosa.mu_compress(x, quantize=True) >>> y array([-128, -124, -120, -116, -110, -102, -91, -66, 66, 91, 102, 110, 116, 120, 124, 127]) Compression with quantization and a smaller range >>> y = librosa.mu_compress(x, mu=15, quantize=True) >>> y array([-8, -7, -7, -6, -6, -5, -4, -2, 2, 4, 5, 6, 6, 7, 7, 7]) """ if mu <= 0: raise ParameterError( f"mu-law compression parameter mu={mu} must be strictly positive." ) if np.any(x < -1) or np.any(x > 1): raise ParameterError(f"mu-law input x={x} must be in the range [-1, +1].") x_comp: np.ndarray = np.sign(x) * np.log1p(mu * np.abs(x)) / np.log1p(mu) if quantize: y: np.ndarray = ( np.digitize( x_comp, np.linspace(-1, 1, num=int(1 + mu), endpoint=True), right=True ) - int(mu + 1) // 2 ) return y return x_comp @overload def mu_expand(x: _FloatLike_co, *, mu: float = 255.0, quantize: bool = True) -> np.floating: ... @overload def mu_expand(x: np.ndarray, *, mu: float = 255.0, quantize: bool = True) -> np.ndarray: ... def mu_expand( x: np.ndarray | _FloatLike_co, *, mu: float = 255.0, quantize: bool = True ) -> np.ndarray | np.floating: """Expansion by μ-law This function is the inverse of ``mu_compress``. Given a mu-law compressed signal ``-1 <= x <= 1``, the mu-law expansion is calculated by:: sign(x) * (1 / mu) * ((1 + mu)**abs(x) - 1) Parameters ---------- x : np.ndarray The compressed signal. If ``quantize=True``, values must be in the range [-1, +1]. mu : positive number The compression parameter. Values of the form ``2**n - 1`` (e.g., 15, 31, 63, etc.) are most common. quantize : bool If ``True``, the input is assumed to be quantized to ``1 + mu`` distinct integer values. Returns ------- x_expanded : np.ndarray with values in the range [-1, +1] The mu-law expanded signal. Raises ------ ParameterError If ``x`` has values outside the range [-1, +1] and ``quantize=False`` If ``mu <= 0`` See Also -------- mu_compress Examples -------- Compress and expand without quantization >>> x = np.linspace(-1, 1, num=16) >>> x array([-1. , -0.86666667, -0.73333333, -0.6 , -0.46666667, -0.33333333, -0.2 , -0.06666667, 0.06666667, 0.2 , 0.33333333, 0.46666667, 0.6 , 0.73333333, 0.86666667, 1. ]) >>> y = librosa.mu_compress(x, quantize=False) >>> y array([-1. , -0.97430198, -0.94432361, -0.90834832, -0.86336132, -0.80328309, -0.71255496, -0.52124063, 0.52124063, 0.71255496, 0.80328309, 0.86336132, 0.90834832, 0.94432361, 0.97430198, 1. ]) >>> z = librosa.mu_expand(y, quantize=False) >>> z array([-1. , -0.86666667, -0.73333333, -0.6 , -0.46666667, -0.33333333, -0.2 , -0.06666667, 0.06666667, 0.2 , 0.33333333, 0.46666667, 0.6 , 0.73333333, 0.86666667, 1. ]) Compress and expand with quantization. Note that this necessarily incurs quantization error, particularly for values near +-1. >>> y = librosa.mu_compress(x, quantize=True) >>> y array([-128, -124, -120, -116, -110, -102, -91, -66, 66, 91, 102, 110, 116, 120, 124, 127]) >>> z = librosa.mu_expand(y, quantize=True) array([-1. , -0.84027248, -0.70595818, -0.59301377, -0.4563785 , -0.32155973, -0.19817918, -0.06450245, 0.06450245, 0.19817918, 0.32155973, 0.4563785 , 0.59301377, 0.70595818, 0.84027248, 0.95743702]) """ if mu <= 0: raise ParameterError( f"Inverse mu-law compression parameter mu={mu} must be strictly positive." ) if quantize: x = x * 2.0 / (1 + mu) if np.any(x < -1) or np.any(x > 1): raise ParameterError( f"Inverse mu-law input x={x} must be in the range [-1, +1]." ) y: np.ndarray | np.floating = np.sign(x) / mu * (np.power(1 + mu, np.abs(x)) - 1) return y ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2099245 librosa-1.0.0/librosa/core/constantq.py0000644000175100017510000014322015236640653017574 0ustar00runnerrunner#!/usr/bin/env python # -*- coding: utf-8 -*- """Constant-Q transforms""" from __future__ import annotations import warnings from typing import TYPE_CHECKING import numpy as np import scipy from numba import jit from .. import filters, util from .._cache import cache from ..util.deprecation import Deprecated, rename_kw from ..util.exceptions import ParameterError from . import audio from .convert import cqt_frequencies, note_to_hz from .intervals import interval_frequencies from .pitch import estimate_tuning from .spectrum import istft, stft if TYPE_CHECKING: from typing import Collection from numpy.typing import DTypeLike, NDArray from .._typing import ( RNGLike, SeedLike, _FloatLike_co, _PadMode, _WindowSpec, ) __all__ = ["cqt", "hybrid_cqt", "pseudo_cqt", "icqt", "griffinlim_cqt", "vqt"] # TODO: ivqt, griffinlim_vqt @cache(level=20) def cqt( y: np.ndarray, *, sr: float = 22050, hop_length: int = 512, fmin: _FloatLike_co | None = None, n_bins: int | None = 84, bins_per_octave: int = 12, tuning: float | None = 0.0, filter_scale: float = 1, norm: float | None = 1, sparsity: float = 0.01, window: _WindowSpec = "hann", scale: bool = True, pad_mode: _PadMode = "constant", res_type: str = "soxr_hq", dtype: DTypeLike | None = None, ) -> np.ndarray: """Compute the constant-Q transform of an audio signal. This implementation is based on the recursive sub-sampling method described by [#]_. .. [#] Schoerkhuber, Christian, and Anssi Klapuri. "Constant-Q transform toolbox for music processing." 7th Sound and Music Computing Conference, Barcelona, Spain. 2010. Parameters ---------- y : np.ndarray [shape=(..., n)] audio time series. Multi-channel is supported. sr : number > 0 [scalar] sampling rate of ``y`` hop_length : int > 0 [scalar] number of samples between successive CQT columns. fmin : float > 0 [scalar] Minimum frequency. Defaults to `C1 ~= 32.70 Hz` n_bins : int > 0 or None [scalar] Number of frequency bins, starting at ``fmin``. If `None`, the number of bins will be inferred as the maximum that will fit below `sr/2`. bins_per_octave : int > 0 [scalar] Number of bins per octave tuning : None or float Tuning offset in fractions of a bin. If ``None``, tuning will be automatically estimated from the signal. The minimum frequency of the resulting CQT will be modified to ``fmin * 2**(tuning / bins_per_octave)``. filter_scale : float > 0 Filter scale factor. Small values (<1) use shorter windows for improved time resolution. norm : {inf, -inf, 0, float > 0} Type of norm to use for basis function normalization. See `librosa.util.normalize`. sparsity : float in [0, 1) Sparsify the CQT basis by discarding up to ``sparsity`` fraction of the energy in each basis. Set ``sparsity=0`` to disable sparsification. window : str, tuple, number, or function Window specification for the basis filters. See `filters.get_window` for details. scale : bool If ``True``, scale the CQT response by square-root the length of each channel's filter. This is analogous to ``norm='ortho'`` in FFT. If ``False``, do not scale the CQT. This is analogous to ``norm=None`` in FFT. pad_mode : str Padding mode for centered frame analysis. See also: `librosa.stft` and `numpy.pad`. res_type : str The resampling mode for recursive downsampling. dtype : np.dtype The (complex) data type of the output array. By default, this is inferred to match the numerical precision of the input signal. Returns ------- CQT : np.ndarray [shape=(..., n_bins, t)] Constant-Q value each frequency at each time. See Also -------- vqt librosa.resample librosa.util.normalize Notes ----- This function caches at level 20. Examples -------- Generate and plot a constant-Q power spectrum >>> import matplotlib.pyplot as plt >>> y, sr = librosa.loadx('trumpet') >>> C = np.abs(librosa.cqt(y, sr=sr)) >>> fig, ax = plt.subplots() >>> img = librosa.display.specshow(C, vscale='dBFS', ... sr=sr, x_axis='time', y_axis='cqt_note', ax=ax) >>> ax.set_title('Constant-Q power spectrum') >>> librosa.display.colorbar_db(img) Limit the frequency range >>> C = np.abs(librosa.cqt(y, sr=sr, fmin=librosa.note_to_hz('C2'), ... n_bins=60)) >>> C array([[6.830e-04, 6.361e-04, ..., 7.362e-09, 9.102e-09], [5.366e-04, 4.818e-04, ..., 8.953e-09, 1.067e-08], ..., [4.288e-02, 4.580e-01, ..., 1.529e-05, 5.572e-06], [2.965e-03, 1.508e-01, ..., 8.965e-06, 1.455e-05]]) Using a higher frequency resolution >>> C = np.abs(librosa.cqt(y, sr=sr, fmin=librosa.note_to_hz('C2'), ... n_bins=60 * 2, bins_per_octave=12 * 2)) >>> C array([[5.468e-04, 5.382e-04, ..., 5.911e-09, 6.105e-09], [4.118e-04, 4.014e-04, ..., 7.788e-09, 8.160e-09], ..., [2.780e-03, 1.424e-01, ..., 4.225e-06, 2.388e-05], [5.147e-02, 6.959e-02, ..., 1.694e-05, 5.811e-06]]) """ # CQT is the special case of VQT with gamma=0 return vqt( y=y, sr=sr, hop_length=hop_length, fmin=fmin, n_bins=n_bins, intervals="equal", gamma=0, bins_per_octave=bins_per_octave, tuning=tuning, filter_scale=filter_scale, norm=norm, sparsity=sparsity, window=window, scale=scale, pad_mode=pad_mode, res_type=res_type, dtype=dtype, ) @cache(level=20) def hybrid_cqt( y: np.ndarray, *, sr: float = 22050, hop_length: int = 512, fmin: _FloatLike_co | None = None, n_bins: int | None = 84, bins_per_octave: int = 12, tuning: float | None = 0.0, filter_scale: float = 1, norm: float | None = 1, sparsity: float = 0.01, window: _WindowSpec = "hann", scale: bool = True, pad_mode: _PadMode = "constant", res_type: str = "soxr_hq", dtype: DTypeLike | None = None, ) -> np.ndarray: """Compute the hybrid constant-Q transform of an audio signal. Here, the hybrid CQT uses the pseudo CQT for higher frequencies where the hop_length is longer than half the filter length and the full CQT for lower frequencies. Parameters ---------- y : np.ndarray [shape=(..., n)] audio time series. Multi-channel is supported. sr : number > 0 [scalar] sampling rate of ``y`` hop_length : int > 0 [scalar] number of samples between successive CQT columns. fmin : float > 0 [scalar] Minimum frequency. Defaults to `C1 ~= 32.70 Hz` n_bins : int > 0 or None [scalar] Number of frequency bins, starting at ``fmin`` If `None` the number of bins will be inferred as the maximum that will fit below `sr/2`. bins_per_octave : int > 0 [scalar] Number of bins per octave tuning : None or float Tuning offset in fractions of a bin. If ``None``, tuning will be automatically estimated from the signal. The minimum frequency of the resulting CQT will be modified to ``fmin * 2**(tuning / bins_per_octave)``. filter_scale : float > 0 Filter filter_scale factor. Larger values use longer windows. norm : {inf, -inf, 0, float > 0} Type of norm to use for basis function normalization. See `librosa.util.normalize`. sparsity : float in [0, 1) Sparsify the CQT basis by discarding up to ``sparsity`` fraction of the energy in each basis. Set ``sparsity=0`` to disable sparsification. window : str, tuple, number, or function Window specification for the basis filters. See `filters.get_window` for details. scale : bool If ``True``, scale the CQT response by square-root the length of each channel's filter. This is analogous to ``norm='ortho'`` in FFT. If ``False``, do not scale the CQT. This is analogous to ``norm=None`` in FFT. pad_mode : str Padding mode for centered frame analysis. See also: `librosa.stft` and `numpy.pad`. res_type : str Resampling mode. See `librosa.cqt` for details. dtype : np.dtype, optional The complex dtype to use for computing the CQT. By default, this is inferred to match the precision of the input signal. Returns ------- CQT : np.ndarray [shape=(..., n_bins, t), dtype=np.float] Constant-Q energy for each frequency at each time. See Also -------- cqt pseudo_cqt Notes ----- This function caches at level 20. """ if fmin is None: # C1 by default fmin = note_to_hz("C1") if tuning is None: tuning = estimate_tuning(y=y, sr=sr, bins_per_octave=bins_per_octave) # Apply tuning correction fmin = fmin * 2.0 ** (tuning / bins_per_octave) if fmin >= sr / 2: raise ParameterError(f"fmin={fmin} must be less than sr/2={sr/2}") if n_bins is None: n_bins = int(np.ceil(bins_per_octave * (np.log2(sr) - np.log2(fmin)))) auto_n_bins = True else: auto_n_bins = False # Get all CQT frequencies freqs = cqt_frequencies(n_bins, fmin=fmin, bins_per_octave=bins_per_octave) if auto_n_bins: # Calling with gamma=0 here since this is CQT freqs = __clip_freqs(freqs, window, filter_scale, 0, sr) n_bins = len(freqs) # Pre-compute alpha if n_bins == 1: alpha = __et_relative_bw(bins_per_octave) else: alpha = filters._relative_bandwidth(freqs=freqs) # Compute the length of each constant-Q basis function lengths, filter_cutoff = filters.wavelet_lengths( freqs=freqs, sr=sr, filter_scale=filter_scale, window=window, alpha=alpha ) if filter_cutoff > sr / 2: raise ParameterError( f"Filter cutoff frequency {filter_cutoff} exceeds Nyquist frequency {sr/2}. " "Try reducing the number of frequency bins." ) # Determine which filters to use with Pseudo CQT # These are the ones that fit within 2 hop lengths after padding pseudo_filters = 2.0 ** np.ceil(np.log2(lengths)) < 2 * hop_length n_bins_pseudo = int(np.sum(pseudo_filters)) n_bins_full = n_bins - n_bins_pseudo cqt_resp = [] if n_bins_pseudo > 0: fmin_pseudo = np.min(freqs[pseudo_filters]) cqt_resp.append( pseudo_cqt( y, sr=sr, hop_length=hop_length, fmin=fmin_pseudo, n_bins=n_bins_pseudo, bins_per_octave=bins_per_octave, filter_scale=filter_scale, norm=norm, sparsity=sparsity, window=window, scale=scale, pad_mode=pad_mode, dtype=dtype, ) ) if n_bins_full > 0: cqt_resp.append( np.abs( cqt( y, sr=sr, hop_length=hop_length, fmin=fmin, n_bins=n_bins_full, bins_per_octave=bins_per_octave, filter_scale=filter_scale, norm=norm, sparsity=sparsity, window=window, scale=scale, pad_mode=pad_mode, res_type=res_type, dtype=dtype, ) ) ) # Propagate dtype from the last component return __trim_stack(cqt_resp, n_bins, cqt_resp[-1].dtype) @cache(level=20) def pseudo_cqt( y: np.ndarray, *, sr: float = 22050, hop_length: int = 512, fmin: _FloatLike_co | None = None, n_bins: int | None = 84, bins_per_octave: int = 12, tuning: float | None = 0.0, filter_scale: float = 1, norm: float | None = 1, sparsity: float = 0.01, window: _WindowSpec = "hann", scale: bool = True, pad_mode: _PadMode = "constant", dtype: DTypeLike | None = None, ) -> np.ndarray: """Compute the pseudo constant-Q transform of an audio signal. This uses a single fft size that is the smallest power of 2 that is greater than or equal to the max of: 1. The longest CQT filter 2. 2x the hop_length Parameters ---------- y : np.ndarray [shape=(..., n)] audio time series. Multi-channel is supported. sr : number > 0 [scalar] sampling rate of ``y`` hop_length : int > 0 [scalar] number of samples between successive CQT columns. fmin : float > 0 [scalar] Minimum frequency. Defaults to `C1 ~= 32.70 Hz` n_bins : int > 0 or None [scalar] Number of frequency bins, starting at ``fmin`` If `None` the number of bins will be inferred as the maximum that will fit below `sr/2`. bins_per_octave : int > 0 [scalar] Number of bins per octave tuning : None or float Tuning offset in fractions of a bin. If ``None``, tuning will be automatically estimated from the signal. The minimum frequency of the resulting CQT will be modified to ``fmin * 2**(tuning / bins_per_octave)``. filter_scale : float > 0 Filter filter_scale factor. Larger values use longer windows. norm : {inf, -inf, 0, float > 0} Type of norm to use for basis function normalization. See `librosa.util.normalize`. sparsity : float in [0, 1) Sparsify the CQT basis by discarding up to ``sparsity`` fraction of the energy in each basis. Set ``sparsity=0`` to disable sparsification. window : str, tuple, number, or function Window specification for the basis filters. See `filters.get_window` for details. scale : bool If ``True``, scale the CQT response by square-root the length of each channel's filter. This is analogous to ``norm='ortho'`` in FFT. If ``False``, do not scale the CQT. This is analogous to ``norm=None`` in FFT. pad_mode : str Padding mode for centered frame analysis. See also: `librosa.stft` and `numpy.pad`. dtype : np.dtype, optional The complex data type for CQT calculations. By default, this is inferred to match the precision of the input signal. Returns ------- CQT : np.ndarray [shape=(..., n_bins, t), dtype=np.float] Pseudo Constant-Q energy for each frequency at each time. Notes ----- This function caches at level 20. """ if fmin is None: # C1 by default fmin = note_to_hz("C1") if tuning is None: tuning = estimate_tuning(y=y, sr=sr, bins_per_octave=bins_per_octave) if dtype is None: dtype = util.dtype_r2c(y.dtype) # Apply tuning correction fmin = fmin * 2.0 ** (tuning / bins_per_octave) if fmin >= sr / 2: raise ParameterError(f"fmin={fmin} must be less than sr/2={sr/2}") if n_bins is None: n_bins = int(np.ceil(bins_per_octave * (np.log2(sr) - np.log2(fmin)))) auto_n_bins = True else: auto_n_bins = False freqs = cqt_frequencies(fmin=fmin, n_bins=n_bins, bins_per_octave=bins_per_octave) if auto_n_bins: # Calling with gamma=0 here since this is CQT freqs = __clip_freqs(freqs, window, filter_scale, 0, sr) n_bins = len(freqs) if n_bins == 1: alpha = __et_relative_bw(bins_per_octave) else: alpha = filters._relative_bandwidth(freqs=freqs) lengths, filter_cutoff = filters.wavelet_lengths( freqs=freqs, sr=sr, window=window, filter_scale=filter_scale, alpha=alpha ) if filter_cutoff > sr / 2: raise ParameterError( f"Filter cutoff frequency {filter_cutoff} exceeds Nyquist frequency {sr/2}. " "Try reducing the number of frequency bins." ) fft_basis, n_fft, _ = __vqt_filter_fft( sr, freqs, filter_scale, norm, sparsity, hop_length=hop_length, window=window, dtype=dtype, alpha=alpha, ) fft_basis = np.abs(fft_basis) # Compute the magnitude-only CQT response C: np.ndarray = __cqt_response( y, n_fft, hop_length, fft_basis, pad_mode, window="hann", dtype=dtype, phase=False, ) if scale: C /= np.sqrt(n_fft) else: # reshape lengths to match dimension properly lengths = util.expand_to(lengths, ndim=C.ndim, axes=-2) C *= np.sqrt(lengths / n_fft) return C @cache(level=40) def icqt( C: np.ndarray, *, sr: float = 22050, hop_length: int = 512, fmin: _FloatLike_co | None = None, bins_per_octave: int = 12, tuning: float = 0.0, filter_scale: float = 1, norm: float | None = 1, sparsity: float = 0.01, window: _WindowSpec = "hann", scale: bool = True, length: int | None = None, res_type: str = "soxr_hq", dtype: DTypeLike | None = None, ) -> np.ndarray: """Compute the inverse constant-Q transform. Given a constant-Q transform representation ``C`` of an audio signal ``y``, this function produces an approximation ``y_hat``. Parameters ---------- C : np.ndarray, [shape=(..., n_bins, n_frames)] Constant-Q representation as produced by `cqt` sr : number > 0 [scalar] sampling rate of the signal hop_length : int > 0 [scalar] number of samples between successive frames fmin : float > 0 [scalar] Minimum frequency. Defaults to `C1 ~= 32.70 Hz` bins_per_octave : int > 0 [scalar] Number of bins per octave tuning : float [scalar] Tuning offset in fractions of a bin. The minimum frequency of the CQT will be modified to ``fmin * 2**(tuning / bins_per_octave)``. filter_scale : float > 0 [scalar] Filter scale factor. Small values (<1) use shorter windows for improved time resolution. norm : {inf, -inf, 0, float > 0} Type of norm to use for basis function normalization. See `librosa.util.normalize`. sparsity : float in [0, 1) Sparsify the CQT basis by discarding up to ``sparsity`` fraction of the energy in each basis. Set ``sparsity=0`` to disable sparsification. window : str, tuple, number, or function Window specification for the basis filters. See `filters.get_window` for details. scale : bool If ``True``, scale the CQT response by square-root the length of each channel's filter. This is analogous to ``norm='ortho'`` in FFT. If ``False``, do not scale the CQT. This is analogous to ``norm=None`` in FFT. length : int > 0, optional If provided, the output ``y`` is zero-padded or clipped to exactly ``length`` samples. res_type : str Resampling mode. See `librosa.resample` for supported modes. dtype : numeric type Real numeric type for ``y``. Default is inferred to match the numerical precision of the input CQT. Returns ------- y : np.ndarray, [shape=(..., n_samples), dtype=np.float] Audio time-series reconstructed from the CQT representation. See Also -------- cqt librosa.resample Notes ----- This function caches at level 40. Examples -------- Using default parameters >>> y, sr = librosa.loadx('trumpet') >>> C = librosa.cqt(y=y, sr=sr) >>> y_hat = librosa.icqt(C=C, sr=sr) Or with a different hop length and frequency resolution: >>> hop_length = 256 >>> bins_per_octave = 12 * 3 >>> C = librosa.cqt(y=y, sr=sr, hop_length=256, n_bins=7*bins_per_octave, ... bins_per_octave=bins_per_octave) >>> y_hat = librosa.icqt(C=C, sr=sr, hop_length=hop_length, ... bins_per_octave=bins_per_octave) """ if fmin is None: fmin = note_to_hz("C1") # Apply tuning correction fmin = fmin * 2.0 ** (tuning / bins_per_octave) # Get the top octave of frequencies n_bins = C.shape[-2] n_octaves = int(np.ceil(float(n_bins) / bins_per_octave)) freqs = cqt_frequencies(fmin=fmin, n_bins=n_bins, bins_per_octave=bins_per_octave) if n_bins == 1: alpha = __et_relative_bw(bins_per_octave) else: alpha = filters._relative_bandwidth(freqs=freqs) lengths, _f_cutoff = filters.wavelet_lengths( freqs=freqs, sr=sr, window=window, filter_scale=filter_scale, alpha=alpha ) # Trim the CQT to only what's necessary for reconstruction if length is not None: n_frames = int(np.ceil((length + max(lengths)) / hop_length)) C = C[..., :n_frames] C_scale = np.sqrt(lengths) # This shape array will be used for broadcasting the basis scale # we'll have to adapt this per octave within the loop y: np.ndarray | None = None # Assume the top octave is at the full rate srs = [sr] hops = [hop_length] for _i in range(n_octaves - 1): if hops[0] % 2 == 0: # We can downsample: srs.insert(0, srs[0] * 0.5) hops.insert(0, hops[0] // 2) else: # We're out of downsamplings, carry forward srs.insert(0, srs[0]) hops.insert(0, hops[0]) for i, (my_sr, my_hop) in enumerate(zip(srs, hops, strict=True)): # How many filters are in this octave? n_filters = min(bins_per_octave, n_bins - bins_per_octave * i) # Slice out the current octave sl = slice(bins_per_octave * i, bins_per_octave * i + n_filters) fft_basis, n_fft, _ = __vqt_filter_fft( my_sr, freqs[sl], filter_scale, norm, sparsity, window=window, alpha=alpha[sl], ) # Transpose the basis inv_basis = fft_basis.conjugate().T.toarray() # Compute each filter's frequency-domain power freq_power = 1 / np.sum(util.abs2(inv_basis), axis=0) # Compensate for length normalization in the forward transform freq_power *= n_fft / lengths[sl] # Inverse-project the basis for each octave if scale: # scale=True ==> re-scale by sqrt(lengths) D_oct = np.einsum( "fc,c,c,...ct->...ft", inv_basis, C_scale[sl], freq_power, C[..., sl, :], optimize=True, ) else: D_oct = np.einsum( "fc,c,...ct->...ft", inv_basis, freq_power, C[..., sl, :], optimize=True ) y_oct = istft(D_oct, window="ones", hop_length=my_hop, dtype=dtype) y_oct = audio.resample( y_oct, orig_sr=1, target_sr=sr // my_sr, res_type=res_type, scale=False, fix=False, ) if y is None: y = y_oct else: y[..., : y_oct.shape[-1]] += y_oct # make mypy happy assert y is not None if length: y = util.fix_length(y, size=length) return y @cache(level=20) def vqt( y: np.ndarray, *, sr: float = 22050, hop_length: int = 512, fmin: _FloatLike_co | None = None, n_bins: int | None = 84, intervals: str | Collection[float] = "equal", gamma: float | None = None, bins_per_octave: int = 12, tuning: float | None = 0.0, filter_scale: float = 1, norm: float | None = 1, sparsity: float = 0.01, window: _WindowSpec = "hann", scale: bool = True, pad_mode: _PadMode = "constant", res_type: str = "soxr_hq", dtype: DTypeLike | None = None, ) -> np.ndarray: """Compute the variable-Q transform of an audio signal. This implementation is based on the recursive sub-sampling method described by [#]_. .. [#] Schörkhuber, Christian, Anssi Klapuri, Nicki Holighaus, and Monika Dörfler. "A Matlab toolbox for efficient perfect reconstruction time-frequency transforms with log-frequency resolution." In Audio Engineering Society Conference: 53rd International Conference: Semantic Audio. Audio Engineering Society, 2014. Parameters ---------- y : np.ndarray [shape=(..., n)] audio time series. Multi-channel is supported. sr : number > 0 [scalar] sampling rate of ``y`` hop_length : int > 0 [scalar] number of samples between successive VQT columns. fmin : float > 0 [scalar] Minimum frequency. Defaults to `C1 ~= 32.70 Hz` n_bins : int > 0 or None [scalar] Number of frequency bins, starting at ``fmin`` If `None`, the number of bins will be inferred as the maximum that will fit below `sr/2`. intervals : str or array of floats in [1, 2) Either a string specification for an interval set, e.g., `'equal'`, `'pythagorean'`, `'ji3'`, etc. or an array of intervals expressed as numbers between 1 and 2. .. see also:: librosa.interval_frequencies gamma : number > 0 [scalar] Bandwidth offset for determining filter lengths. If ``gamma=0``, produces the constant-Q transform. If 'gamma=None', gamma will be calculated such that filter bandwidths are equal to a constant fraction of the equivalent rectangular bandwidths (ERB). This is accomplished by solving for the gamma which gives:: B_k = alpha * f_k + gamma = C * ERB(f_k), where ``B_k`` is the bandwidth of filter ``k`` with center frequency ``f_k``, alpha is the inverse of what would be the constant Q-factor, and ``C = alpha / 0.108`` is the constant fraction across all filters. Here we use ``ERB(f_k) = 24.7 + 0.108 * f_k``, the best-fit curve derived from experimental data in [#]_. .. [#] Glasberg, Brian R., and Brian CJ Moore. "Derivation of auditory filter shapes from notched-noise data." Hearing research 47.1-2 (1990): 103-138. bins_per_octave : int > 0 [scalar] Number of bins per octave tuning : None or float Tuning offset in fractions of a bin. If ``None``, tuning will be automatically estimated from the signal. The minimum frequency of the resulting VQT will be modified to ``fmin * 2**(tuning / bins_per_octave)``. filter_scale : float > 0 Filter scale factor. Small values (<1) use shorter windows for improved time resolution. norm : {inf, -inf, 0, float > 0} Type of norm to use for basis function normalization. See `librosa.util.normalize`. sparsity : float in [0, 1) Sparsify the VQT basis by discarding up to ``sparsity`` fraction of the energy in each basis. Set ``sparsity=0`` to disable sparsification. window : str, tuple, number, or function Window specification for the basis filters. See `filters.get_window` for details. scale : bool If ``True``, scale the VQT response by square-root the length of each channel's filter. This is analogous to ``norm='ortho'`` in FFT. If ``False``, do not scale the VQT. This is analogous to ``norm=None`` in FFT. pad_mode : str Padding mode for centered frame analysis. See also: `librosa.stft` and `numpy.pad`. res_type : str The resampling mode for recursive downsampling. dtype : np.dtype The dtype of the output array. By default, this is inferred to match the numerical precision of the input signal. Returns ------- VQT : np.ndarray [shape=(..., n_bins, t), dtype=np.complex] Variable-Q value each frequency at each time. See Also -------- cqt Notes ----- This function caches at level 20. Examples -------- Generate and plot a variable-Q power spectrum >>> import matplotlib.pyplot as plt >>> y, sr = librosa.loadx('choice', duration=5) >>> C = np.abs(librosa.cqt(y, sr=sr)) >>> V = np.abs(librosa.vqt(y, sr=sr)) >>> fig, ax = plt.subplots(nrows=2, sharex=True, sharey=True) >>> librosa.display.specshow(C, vscale='dBFS', ... sr=sr, x_axis='time', y_axis='cqt_note', ax=ax[0]) >>> ax[0].set(title='Constant-Q power spectrum', xlabel=None) >>> ax[0].label_outer() >>> img = librosa.display.specshow(V, vscale='dBFS', ... sr=sr, x_axis='time', y_axis='cqt_note', ax=ax[1]) >>> ax[1].set_title('Variable-Q power spectrum') >>> librosa.display.colorbar_db(img, ax=ax) """ # If intervals are provided as an array, override BPO if not isinstance(intervals, str): bins_per_octave = len(intervals) if fmin is None: # C1 by default fmin = note_to_hz("C1") if tuning is None: tuning = estimate_tuning(y=y, sr=sr, bins_per_octave=bins_per_octave) if dtype is None: dtype = util.dtype_r2c(y.dtype) # Apply tuning correction fmin = fmin * 2.0 ** (tuning / bins_per_octave) if fmin >= sr / 2: raise ParameterError(f"fmin={fmin} must be less than sr/2={sr/2}") if n_bins is None: # If n_bins is None, we need to compute the number of bins to reach sr/2 # Over-allocating by one octave buys us enough buffer to be safe. # We'll clip back down later as needed. n_bins = int(np.ceil(bins_per_octave * (np.log2(sr) - np.log2(fmin)))) # Equivalently: bins_per_octave * (1 + log2(sr/2 / fmin)) auto_n_bins = True else: auto_n_bins = False freqs = interval_frequencies( n_bins=n_bins, fmin=fmin, intervals=intervals, bins_per_octave=bins_per_octave, sort=True, ) if auto_n_bins: freqs = __clip_freqs(freqs, window, filter_scale, gamma, sr) n_bins = len(freqs) if n_bins == 1: alpha = __et_relative_bw(bins_per_octave) else: alpha = filters._relative_bandwidth(freqs=freqs) lengths, filter_cutoff = filters.wavelet_lengths( freqs=freqs, sr=sr, window=window, filter_scale=filter_scale, gamma=gamma, alpha=alpha, ) # Determine required resampling quality nyquist = sr / 2.0 if filter_cutoff > nyquist: freqs_top = freqs[-bins_per_octave:] fmax_t: float = np.max(freqs_top) raise ParameterError( f"Wavelet basis with max frequency={fmax_t} would exceed the Nyquist frequency={nyquist}. " "Try reducing the number of frequency bins." ) # How many octaves are we dealing with? n_octaves = int(np.ceil(float(n_bins) / bins_per_octave)) n_filters = min(bins_per_octave, n_bins) y, sr, hop_length = __early_downsample( y, sr, hop_length, res_type, n_octaves, nyquist, filter_cutoff, scale ) vqt_resp = [] # Iterate down the octaves my_y, my_sr, my_hop = y, sr, hop_length for i in range(n_octaves): # Slice out the current octave of filters if i == 0: sl = slice(-n_filters, None) else: sl = slice(-n_filters * (i + 1), -n_filters * i) # This may be incorrect with early downsampling freqs_oct = freqs[sl] alpha_oct = alpha[sl] fft_basis, n_fft, _ = __vqt_filter_fft( my_sr, freqs_oct, filter_scale, norm, sparsity, window=window, gamma=gamma, dtype=dtype, alpha=alpha_oct, ) # Re-scale the filters to compensate for downsampling fft_basis[:] *= np.sqrt(sr / my_sr) # Compute the vqt filter response and append to the stack vqt_resp.append( __cqt_response(my_y, n_fft, my_hop, fft_basis, pad_mode, dtype=dtype) ) # We can downsample here if the hop length is even, and the highest frequency # in the next octave down is comfortably below the target Nyquist frequency. # We'll assume that rolloff from downsampling starts at ~80% Nyquist. # This is equivalent to 40% of the target sampling rate, or 20% of the current sampling # rate. # Only downsample if we're not in the last octave. if i < n_octaves - 1: f_max_next = freqs[sl.start - 1] if my_hop % 2 == 0 and f_max_next <= my_sr / 5: my_hop //= 2 my_sr /= 2.0 my_y = audio.resample( my_y, orig_sr=2, target_sr=1, res_type=res_type, scale=True ) V = __trim_stack(vqt_resp, n_bins, dtype) if scale: # Recompute lengths here because early downsampling may have changed # our sampling rate lengths, _ = filters.wavelet_lengths( freqs=freqs, sr=sr, window=window, filter_scale=filter_scale, gamma=gamma, alpha=alpha, ) # reshape lengths to match V shape lengths = util.expand_to(lengths, ndim=V.ndim, axes=-2) V /= np.sqrt(lengths) return V @cache(level=10) def __vqt_filter_fft( sr, freqs, filter_scale, norm, sparsity, hop_length=None, window="hann", gamma=0.0, dtype=np.complex64, alpha=None, ): """Generate the frequency domain variable-Q filter basis.""" basis, lengths = filters.wavelet( freqs=freqs, sr=sr, filter_scale=filter_scale, norm=norm, pad_fft=True, window=window, gamma=gamma, alpha=alpha, ) # Filters are padded up to the nearest integral power of 2 n_fft = basis.shape[1] if hop_length is not None and n_fft < 2.0 ** (1 + np.ceil(np.log2(hop_length))): n_fft = int(2.0 ** (1 + np.ceil(np.log2(hop_length)))) # re-normalize bases with respect to the FFT window length basis *= lengths[:, np.newaxis] / float(n_fft) # FFT and retain only the non-negative frequencies # Note: in principle we could use an rfft here, but scipy.fft only allows # real inputs, so we'd have to call twice. That negates the speed advantage # of using rfft in the first place. fft_basis = scipy.fft.fft(basis, n=n_fft, axis=1)[:, : (n_fft // 2) + 1] # sparsify the basis fft_basis = util.sparsify_rows(fft_basis, quantile=sparsity, dtype=dtype) return fft_basis, n_fft, lengths def __trim_stack( cqt_resp: list[np.ndarray], n_bins: int, dtype: DTypeLike ) -> np.ndarray: """Trim and stack a collection of CQT responses""" max_col = min(c_i.shape[-1] for c_i in cqt_resp) # Grab any leading dimensions shape = list(cqt_resp[0].shape) shape[-2] = n_bins shape[-1] = max_col cqt_out = np.empty(shape, dtype=dtype, order="F") # Copy per-octave data into output array end = n_bins for c_i in cqt_resp: # By default, take the whole octave n_oct = c_i.shape[-2] # If the whole octave is more than we can fit, # take the highest bins from c_i if end < n_oct: cqt_out[..., :end, :] = c_i[..., -end:, :max_col] else: cqt_out[..., end - n_oct : end, :] = c_i[..., :max_col] end -= n_oct return cqt_out def __cqt_response( y, n_fft, hop_length, fft_basis, mode, window="ones", phase=True, dtype=None ): """Compute the filter response with a target STFT hop.""" # Compute the STFT matrix D = stft( y, n_fft=n_fft, hop_length=hop_length, window=window, pad_mode=mode, dtype=dtype ) if not phase: D = np.abs(D) # Reshape D to Dr Dr = D.reshape((-1, D.shape[-2], D.shape[-1])) output_flat = np.empty( (Dr.shape[0], fft_basis.shape[0], Dr.shape[-1]), dtype=D.dtype ) # iterate over channels # project fft_basis.dot(Dr[i]) for i in range(Dr.shape[0]): output_flat[i] = fft_basis.dot(Dr[i]) # reshape Dr to match D's leading dimensions again shape = list(D.shape) shape[-2] = fft_basis.shape[0] return output_flat.reshape(shape) def __early_downsample_count(nyquist, filter_cutoff, hop_length, n_octaves): """Compute the number of early downsampling operations""" downsample_count1 = max(0, int(np.ceil(np.log2(nyquist / filter_cutoff)) - 1) - 1) num_twos = __num_two_factors(hop_length) downsample_count2 = max(0, num_twos - n_octaves + 1) return min(downsample_count1, downsample_count2) def __early_downsample( y, sr, hop_length, res_type, n_octaves, nyquist, filter_cutoff, scale ): """Perform early downsampling on an audio signal, if it applies.""" downsample_count = __early_downsample_count( nyquist, filter_cutoff, hop_length, n_octaves ) if downsample_count > 0: downsample_factor = 2 ** (downsample_count) hop_length //= downsample_factor if y.shape[-1] < downsample_factor: raise ParameterError( f"Input signal length={len(y):d} is too short for " f"{n_octaves:d}-octave CQT" ) new_sr = sr / float(downsample_factor) y = audio.resample( y, orig_sr=downsample_factor, target_sr=1, res_type=res_type, scale=True ) # If we're not going to length-scale after CQT, we # need to compensate for the downsampling factor here if not scale: y *= np.sqrt(downsample_factor) sr = new_sr return y, sr, hop_length @jit(nopython=True, cache=True) def __num_two_factors(x): """Return how many times integer x can be evenly divided by 2. Returns 0 for non-positive integers. """ if x <= 0: return 0 num_twos = 0 while x % 2 == 0: num_twos += 1 x //= 2 return num_twos def griffinlim_cqt( C: np.ndarray, *, n_iter: int = 32, sr: float = 22050, hop_length: int = 512, fmin: _FloatLike_co | None = None, bins_per_octave: int = 12, tuning: float = 0.0, filter_scale: float = 1, norm: float | None = 1, sparsity: float = 0.01, window: _WindowSpec = "hann", scale: bool = True, pad_mode: _PadMode = "constant", res_type: str = "soxr_hq", dtype: DTypeLike | None = None, length: int | None = None, momentum: float = 0.99, init: str | None = "random", rng: RNGLike | SeedLike | None = None, random_state: int | np.random.RandomState | np.random.Generator | Deprecated | None = Deprecated(), ) -> np.ndarray: """Approximate constant-Q magnitude spectrogram inversion using the "fast" Griffin-Lim algorithm. Given the magnitude of a constant-Q spectrogram (``C``), the algorithm randomly initializes phase estimates, and then alternates forward- and inverse-CQT operations. [#]_ This implementation is based on the (fast) Griffin-Lim method for Short-time Fourier Transforms, [#]_ but adapted for use with constant-Q spectrograms. .. [#] D. W. Griffin and J. S. Lim, "Signal estimation from modified short-time Fourier transform," IEEE Trans. ASSP, vol.32, no.2, pp.236--243, Apr. 1984. .. [#] Perraudin, N., Balazs, P., & Søndergaard, P. L. "A fast Griffin-Lim algorithm," IEEE Workshop on Applications of Signal Processing to Audio and Acoustics (pp. 1--4), Oct. 2013. Parameters ---------- C : np.ndarray [shape=(..., n_bins, n_frames)] The constant-Q magnitude spectrogram n_iter : int > 0 The number of iterations to run sr : number > 0 Audio sampling rate hop_length : int > 0 The hop length of the CQT fmin : number > 0 Minimum frequency for the CQT. If not provided, it defaults to `C1`. bins_per_octave : int > 0 Number of bins per octave tuning : float Tuning deviation from A440, in fractions of a bin filter_scale : float > 0 Filter scale factor. Small values (<1) use shorter windows for improved time resolution. norm : {inf, -inf, 0, float > 0} Type of norm to use for basis function normalization. See `librosa.util.normalize`. sparsity : float in [0, 1) Sparsify the CQT basis by discarding up to ``sparsity`` fraction of the energy in each basis. Set ``sparsity=0`` to disable sparsification. window : str, tuple, or function Window specification for the basis filters. See `filters.get_window` for details. scale : bool If ``True``, scale the CQT response by square-root the length of each channel's filter. This is analogous to ``norm='ortho'`` in FFT. If ``False``, do not scale the CQT. This is analogous to ``norm=None`` in FFT. pad_mode : str Padding mode for centered frame analysis. See also: `librosa.stft` and `numpy.pad`. res_type : str The resampling mode for recursive downsampling. See ``librosa.resample`` for a list of available options. dtype : numeric type Real numeric type for ``y``. Default is inferred to match the precision of the input CQT. length : int > 0, optional If provided, the output ``y`` is zero-padded or clipped to exactly ``length`` samples. momentum : float > 0 The momentum parameter for fast Griffin-Lim. Setting this to 0 recovers the original Griffin-Lim method. Values near 1 can lead to faster convergence, but above 1 may not converge. init : None or 'random' [default] If 'random' (the default), then phase values are initialized randomly according to ``random_state``. This is recommended when the input ``C`` is a magnitude spectrogram with no initial phase estimates. If ``None``, then the phase is initialized from ``C``. This is useful when an initial guess for phase can be provided, or when you want to resume Griffin-Lim from a previous output. rng : None, int, sequence of int, np.random.Generator, or np.random.RandomState Pseudorandom number generator state. When `rng` is None, a new `numpy.random.Generator` is created using entropy from the operating system. Types other than `numpy.random.Generator` are passed to `numpy.random.default_rng` to instantiate a ``Generator``. random_state : None, int, np.random.RandomState, or np.random.Generator .. warning:: This parameter is deprecated in 1.0.0 and will be removed in 1.2.0. If int, random_state is the seed used by the random number generator for phase initialization. If `np.random.RandomState` or `np.random.Generator` instance, the random number generator itself. If ``None``, defaults to the `np.random.default_rng()` object. An exception is raised if both `rng` and `random_state` are provided. Returns ------- y : np.ndarray [shape=(..., n)] time-domain signal reconstructed from ``C`` See Also -------- cqt icqt griffinlim filters.get_window resample Examples -------- A basic CQT inverse example >>> y, sr = librosa.loadx('trumpet', sr=None) >>> # Get the CQT magnitude, 7 octaves at 36 bins per octave >>> C = np.abs(librosa.cqt(y=y, sr=sr, bins_per_octave=36, n_bins=7*36)) >>> # Invert using Griffin-Lim >>> y_inv = librosa.griffinlim_cqt(C, sr=sr, bins_per_octave=36) >>> # And invert without estimating phase >>> y_icqt = librosa.icqt(C, sr=sr, bins_per_octave=36) Wave-plot the results >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=3, sharex=True, sharey=True) >>> librosa.display.waveshow(y, sr=sr, color='C0', ax=ax[0]) >>> ax[0].set(title='Original', xlabel=None) >>> ax[0].label_outer() >>> librosa.display.waveshow(y_inv, sr=sr, color='C1', ax=ax[1]) >>> ax[1].set(title='Griffin-Lim reconstruction', xlabel=None) >>> ax[1].label_outer() >>> librosa.display.waveshow(y_icqt, sr=sr, color='C2', ax=ax[2]) >>> ax[2].set(title='Magnitude-only icqt reconstruction') """ if fmin is None: fmin = note_to_hz("C1") if not isinstance(random_state, Deprecated): if rng is not None: raise ParameterError( f"Both random_state={random_state!r} and rng={rng!r} were provided. " "Please use only the rng parameter." ) # Otherwise transfer the state object and throw a deprecation warning rng = rename_kw( old_name="random_state", old_value=random_state, new_name="rng", new_value=rng, version_deprecated="1.0.0", version_removed="1.2.0", ) # Coerce the various input types to a proper Generator # This branch is necessary until we bump to numpy 2.2 if not isinstance(rng, np.random.RandomState): rng = np.random.default_rng(rng) if momentum > 1: warnings.warn( f"Griffin-Lim with momentum={momentum} > 1 can be unstable. " "Proceed with caution!", stacklevel=2, ) elif momentum < 0: raise ParameterError(f"griffinlim_cqt() called with momentum={momentum} < 0") # using complex64 will keep the result to minimal necessary precision angles = np.empty(C.shape, dtype=np.complex64) eps = util.tiny(angles) if init == "random": # randomly initialize the phase angles[:] = util.phasor(2 * np.pi * rng.random(size=C.shape)) elif init is None: # Initialize an all ones complex matrix angles[:] = 1.0 else: raise ParameterError(f"init={init} must either None or 'random'") # And initialize the previous iterate to 0 rebuilt: np.ndarray = np.array(0.0) for _ in range(n_iter): # Store the previous iterate tprev = rebuilt # Invert with our current estimate of the phases inverse = icqt( C * angles, sr=sr, hop_length=hop_length, bins_per_octave=bins_per_octave, fmin=fmin, tuning=tuning, filter_scale=filter_scale, window=window, length=length, res_type=res_type, norm=norm, scale=scale, sparsity=sparsity, dtype=dtype, ) # Rebuild the spectrogram rebuilt = cqt( inverse, sr=sr, bins_per_octave=bins_per_octave, n_bins=C.shape[-2], hop_length=hop_length, fmin=fmin, tuning=tuning, filter_scale=filter_scale, window=window, norm=norm, scale=scale, sparsity=sparsity, pad_mode=pad_mode, res_type=res_type, ) # Update our phase estimates angles[:] = rebuilt - (momentum / (1 + momentum)) * tprev angles[:] /= np.abs(angles) + eps # Return the final phase estimates return icqt( C * angles, sr=sr, hop_length=hop_length, bins_per_octave=bins_per_octave, tuning=tuning, filter_scale=filter_scale, fmin=fmin, window=window, length=length, res_type=res_type, norm=norm, scale=scale, sparsity=sparsity, dtype=dtype, ) def __et_relative_bw(bins_per_octave: int) -> np.ndarray: """Compute the relative bandwidth coefficient for equal (geometric) freuqency spacing and a give number of bins per octave. This is a special case of the more general `relative_bandwidth` calculation that can be used when only a single basis frequency is used. Parameters ---------- bins_per_octave : int Returns ------- alpha : np.ndarray > 0 Value is cast up to a 1d array to allow slicing """ r = 2 ** (1 / bins_per_octave) return np.atleast_1d((r**2 - 1) / (r**2 + 1)) def __clip_freqs( freqs: NDArray, window: _WindowSpec, filter_scale: float, gamma: float | None, sr: float ) -> NDArray[np.float64]: """Clip a frequency set to avoid exceeding the Nyquist frequency. Parameters ---------- freqs : np.ndarray [shape=(n_bins,)] Frequency set to clip. At least two frequencies are needed. window : str, tuple, or function Window specification for the basis filters. filter_scale : float > 0 Filter scale factor. Small values (<1) use shorter windows for improved time resolution. gamma : float >= 0 or None Bandwidth offset for determining filter lengths. If `None`, use the default ERB-based calculation. sr : number > 0 Audio sampling rate Returns ------- freqs : np.ndarray [shape=(n_bins,)] Frequency set clipped to avoid exceeding the Nyquist frequency. """ logf = np.log2(freqs) window_bw = filters.window_bandwidth(window) # Use the reflection calculation for alpha, under the # assumption that each frequency could be acting as the # fmax. bpo = 1 / np.diff(logf, prepend=0) bpo[0] = 1 / (logf[1] - logf[0]) alpha = (2.0**(2/bpo) - 1) / (2.0 ** (2/bpo) + 1) if gamma is None: gamma_ = alpha * 24.7 / 0.108 else: gamma_ = gamma Q = float(filter_scale) / alpha # Assuming non-monotonicity here: find the # maximum cutoff frequency such that all frequencies # below it land below the Nyquist frequency. f_cutoff = np.maximum.accumulate(freqs * (1 + 0.5 * window_bw / Q) + 0.5 * gamma_) idx = np.searchsorted(f_cutoff, sr / 2.0, side="left") if idx < 1: raise ParameterError(f"Unable to construct wavelet basis for fmin={freqs[0]:.2f} Hz and sr={sr:.2f} Hz.") return freqs[:idx] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2099245 librosa-1.0.0/librosa/core/convert.py0000644000175100017510000023764615236640653017262 0ustar00runnerrunner#!/usr/bin/env python # -*- coding: utf-8 -*- """Unit conversion utilities""" from __future__ import annotations import warnings from typing import TYPE_CHECKING, cast, overload import numpy as np from ..util.decorators import vectorize from ..util.exceptions import ParameterError from . import notation if TYPE_CHECKING: from collections.abc import Sequence from typing import Any, Callable, Iterable, Sized import numpy.typing as npt from .._typing import ( _Array1D, _FloatLike_co, _IntLike_co, _IterableLike, _ScalarOrSequence, _SequenceLike, ) __all__ = [ "frames_to_samples", "frames_to_time", "samples_to_frames", "samples_to_time", "time_to_samples", "time_to_frames", "blocks_to_samples", "blocks_to_frames", "blocks_to_time", "note_to_hz", "note_to_midi", "midi_to_hz", "midi_to_note", "hz_to_note", "hz_to_midi", "hz_to_mel", "hz_to_octs", "hz_to_fjs", "mel_to_hz", "octs_to_hz", "A4_to_tuning", "tuning_to_A4", "fft_frequencies", "cqt_frequencies", "mel_frequencies", "tempo_frequencies", "fourier_tempo_frequencies", "A_weighting", "B_weighting", "C_weighting", "D_weighting", "Z_weighting", "frequency_weighting", "multi_frequency_weighting", "samples_like", "times_like", "midi_to_svara_h", "midi_to_svara_c", "note_to_svara_h", "note_to_svara_c", "hz_to_svara_h", "hz_to_svara_c", ] @overload def frames_to_samples( frames: _IntLike_co, *, hop_length: int = 512, n_fft: int | None = None ) -> np.int_: ... @overload def frames_to_samples( frames: _SequenceLike[_IntLike_co], *, hop_length: int = 512, n_fft: int | None = None, ) -> npt.NDArray[np.int_]: ... def frames_to_samples( frames: _ScalarOrSequence[_IntLike_co], *, hop_length: int = 512, n_fft: int | None = None, ) -> np.int_ | npt.NDArray[np.int_]: """Convert frame indices to audio sample indices. Parameters ---------- frames : number or np.ndarray [shape=(n,)] frame index or vector of frame indices hop_length : int > 0 [scalar] number of samples between successive frames n_fft : None or int > 0 [scalar] Optional: length of the FFT frame. If given, time conversion will include an offset of ``n_fft // 2`` to counteract windowing effects when using a non-centered STFT. Returns ------- times : number or np.ndarray time (in samples) of each given frame number:: times[i] = frames[i] * hop_length See Also -------- frames_to_time : Convert frame indices to time values samples_to_frames : Convert sample indices to frame indices Examples -------- >>> y, sr = librosa.loadx('choice') >>> tempo, beats = librosa.beat.beat_track(y=y, sr=sr) >>> beat_samples = librosa.frames_to_samples(beats, sr=sr) """ offset = 0 if n_fft is not None: offset = int(n_fft // 2) return (np.asanyarray(frames) * hop_length + offset).astype(int)[()] @overload def samples_to_frames( samples: _IntLike_co, *, hop_length: int = ..., n_fft: int | None = ... ) -> np.int_: ... @overload def samples_to_frames( samples: _SequenceLike[_IntLike_co], *, hop_length: int = ..., n_fft: int | None = ..., ) -> npt.NDArray[np.int_]: ... def samples_to_frames( samples: _ScalarOrSequence[_IntLike_co], *, hop_length: int = 512, n_fft: int | None = None, ) -> np.int_ | npt.NDArray[np.int_]: """Convert sample indices into STFT frames. Examples -------- >>> # Get the frame numbers for every 256 samples >>> librosa.samples_to_frames(np.arange(0, 22050, 256)) array([ 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 23, 24, 24, 25, 25, 26, 26, 27, 27, 28, 28, 29, 29, 30, 30, 31, 31, 32, 32, 33, 33, 34, 34, 35, 35, 36, 36, 37, 37, 38, 38, 39, 39, 40, 40, 41, 41, 42, 42, 43]) Parameters ---------- samples : int or np.ndarray [shape=(n,)] sample index or vector of sample indices hop_length : int > 0 [scalar] number of samples between successive frames n_fft : None or int > 0 [scalar] Optional: length of the FFT frame. If given, time conversion will include an offset of ``- n_fft // 2`` to counteract windowing effects in STFT. .. note:: This may result in negative frame indices. Returns ------- frames : int or np.ndarray [shape=(n,), dtype=int] Frame numbers corresponding to the given times:: frames[i] = floor( samples[i] / hop_length ) See Also -------- samples_to_time : Convert sample indices to time values frames_to_samples : Convert frame indices to sample indices """ offset = 0 if n_fft is not None: offset = int(n_fft // 2) samples = np.asanyarray(samples) return np.asarray(np.floor((samples - offset) // hop_length), dtype=int)[()] @overload def frames_to_time( frames: _IntLike_co, *, sr: float = ..., hop_length: int = ..., n_fft: int | None = ..., ) -> np.float64: ... @overload def frames_to_time( frames: _SequenceLike[_IntLike_co], *, sr: float = ..., hop_length: int = ..., n_fft: int | None = ..., ) -> npt.NDArray[np.float64]: ... def frames_to_time( frames: _ScalarOrSequence[_IntLike_co], *, sr: float = 22050, hop_length: int = 512, n_fft: int | None = None, ) -> np.float64 | npt.NDArray[np.float64]: """Convert frame counts to time (seconds). Parameters ---------- frames : np.ndarray [shape=(n,)] frame index or vector of frame indices sr : number > 0 [scalar] audio sampling rate hop_length : int > 0 [scalar] number of samples between successive frames n_fft : None or int > 0 [scalar] Optional: length of the FFT frame. If given, time conversion will include an offset of ``n_fft // 2`` to counteract windowing effects when using a non-centered STFT. Returns ------- times : np.ndarray [shape=(n,)] time (in seconds) of each given frame number:: times[i] = frames[i] * hop_length / sr See Also -------- time_to_frames : Convert time values to frame indices frames_to_samples : Convert frame indices to sample indices Examples -------- >>> y, sr = librosa.loadx('choice') >>> tempo, beats = librosa.beat.beat_track(y=y, sr=sr) >>> beat_times = librosa.frames_to_time(beats, sr=sr) """ samples = frames_to_samples(frames, hop_length=hop_length, n_fft=n_fft) return samples_to_time(samples, sr=sr) @overload def time_to_frames( times: _FloatLike_co, *, sr: float = ..., hop_length: int = ..., n_fft: int | None = ..., ) -> np.int_: ... @overload def time_to_frames( times: _SequenceLike[_FloatLike_co], *, sr: float = ..., hop_length: int = ..., n_fft: int | None = ..., ) -> npt.NDArray[np.int_]: ... def time_to_frames( times: _ScalarOrSequence[_FloatLike_co], *, sr: float = 22050, hop_length: int = 512, n_fft: int | None = None, ) -> np.int_ | npt.NDArray[np.int_]: """Convert time stamps into STFT frames. Parameters ---------- times : np.ndarray [shape=(n,)] time (in seconds) or vector of time values sr : number > 0 [scalar] audio sampling rate hop_length : int > 0 [scalar] number of samples between successive frames n_fft : None or int > 0 [scalar] Optional: length of the FFT frame. If given, time conversion will include an offset of ``- n_fft // 2`` to counteract windowing effects in STFT. .. note:: This may result in negative frame indices. Returns ------- frames : np.ndarray [shape=(n,), dtype=int] Frame numbers corresponding to the given times:: frames[i] = floor( times[i] * sr / hop_length ) See Also -------- frames_to_time : Convert frame indices to time values time_to_samples : Convert time values to sample indices Examples -------- Get the frame numbers for every 100ms >>> librosa.time_to_frames(np.arange(0, 1, 0.1), ... sr=22050, hop_length=512) array([ 0, 4, 8, 12, 17, 21, 25, 30, 34, 38]) """ samples = time_to_samples(times, sr=sr) return samples_to_frames(samples, hop_length=hop_length, n_fft=n_fft) @overload def time_to_samples(times: _FloatLike_co, *, sr: float = ...) -> np.int_: ... @overload def time_to_samples( times: _SequenceLike[_FloatLike_co], *, sr: float = ... ) -> npt.NDArray[np.int_]: ... def time_to_samples( times: _ScalarOrSequence[_FloatLike_co], *, sr: float = 22050 ) -> np.int_ | npt.NDArray[np.int_]: """Convert timestamps (in seconds) to sample indices. Parameters ---------- times : number or np.ndarray Time value or array of time values (in seconds) sr : number > 0 Sampling rate Returns ------- samples : int or np.ndarray [shape=times.shape, dtype=int] Sample indices corresponding to values in ``times`` See Also -------- time_to_frames : Convert time values to frame indices samples_to_time : Convert sample indices to time values Examples -------- >>> librosa.time_to_samples(np.arange(0, 1, 0.1), sr=22050) array([ 0, 2205, 4410, 6615, 8820, 11025, 13230, 15435, 17640, 19845]) """ return (np.asanyarray(times) * sr).astype(int)[()] @overload def samples_to_time(samples: _IntLike_co, *, sr: float = ...) -> np.float64: ... @overload def samples_to_time( samples: _SequenceLike[_IntLike_co], *, sr: float = ... ) -> npt.NDArray[np.float64]: ... def samples_to_time( samples: _ScalarOrSequence[_IntLike_co], *, sr: float = 22050 ) -> np.float64 | npt.NDArray[np.float64]: """Convert sample indices to time (in seconds). Parameters ---------- samples : np.ndarray Sample index or array of sample indices sr : number > 0 Sampling rate Returns ------- times : np.ndarray [shape=samples.shape] Time values corresponding to ``samples`` (in seconds) See Also -------- samples_to_frames : Convert sample indices to frame indices time_to_samples : Convert time values to sample indices Examples -------- Get timestamps corresponding to every 512 samples >>> librosa.samples_to_time(np.arange(0, 22050, 512), sr=22050) array([ 0. , 0.023, 0.046, 0.07 , 0.093, 0.116, 0.139, 0.163, 0.186, 0.209, 0.232, 0.255, 0.279, 0.302, 0.325, 0.348, 0.372, 0.395, 0.418, 0.441, 0.464, 0.488, 0.511, 0.534, 0.557, 0.58 , 0.604, 0.627, 0.65 , 0.673, 0.697, 0.72 , 0.743, 0.766, 0.789, 0.813, 0.836, 0.859, 0.882, 0.906, 0.929, 0.952, 0.975, 0.998]) """ return np.asanyarray(samples)[()] / float(sr) @overload def blocks_to_frames(blocks: _IntLike_co, *, block_length: int) -> np.integer: ... @overload def blocks_to_frames( blocks: _SequenceLike[_IntLike_co], *, block_length: int ) -> npt.NDArray[np.integer]: ... def blocks_to_frames( blocks: _ScalarOrSequence[_IntLike_co], *, block_length: int ) -> np.integer | npt.NDArray[np.integer]: """Convert block indices to frame indices Parameters ---------- blocks : np.ndarray Block index or array of block indices block_length : int > 0 The number of frames per block Returns ------- frames : np.ndarray [shape=samples.shape, dtype=int] The index or indices of frames corresponding to the beginning of each provided block. See Also -------- blocks_to_samples blocks_to_time Examples -------- Get frame indices for each block in a stream >>> filename = librosa.ex('brahms') >>> sr = librosa.get_samplerate(filename) >>> stream = librosa.stream(filename, block_length=16, ... frame_length=2048, hop_length=512) >>> for n, y in enumerate(stream): ... n_frame = librosa.blocks_to_frames(n, block_length=16) """ return block_length * np.asanyarray(blocks)[()] @overload def blocks_to_samples( blocks: _IntLike_co, *, block_length: int, hop_length: int ) -> np.int_: ... @overload def blocks_to_samples( blocks: _SequenceLike[_IntLike_co], *, block_length: int, hop_length: int ) -> npt.NDArray[np.int_]: ... def blocks_to_samples( blocks: _ScalarOrSequence[_IntLike_co], *, block_length: int, hop_length: int ) -> np.int_ | npt.NDArray[np.int_]: """Convert block indices to sample indices Parameters ---------- blocks : np.ndarray Block index or array of block indices block_length : int > 0 The number of frames per block hop_length : int > 0 The number of samples to advance between frames Returns ------- samples : np.ndarray [shape=samples.shape, dtype=int] The index or indices of samples corresponding to the beginning of each provided block. Note that these correspond to the *first* sample index in each block, and are not frame-centered. See Also -------- blocks_to_frames blocks_to_time Examples -------- Get sample indices for each block in a stream >>> filename = librosa.ex('brahms') >>> sr = librosa.get_samplerate(filename) >>> stream = librosa.stream(filename, block_length=16, ... frame_length=2048, hop_length=512) >>> for n, y in enumerate(stream): ... n_sample = librosa.blocks_to_samples(n, block_length=16, ... hop_length=512) """ frames = blocks_to_frames(blocks, block_length=block_length) return frames_to_samples(frames, hop_length=hop_length) @overload def blocks_to_time( blocks: _IntLike_co, *, block_length: int, hop_length: int, sr: float ) -> np.float64: ... @overload def blocks_to_time( blocks: _SequenceLike[_IntLike_co], *, block_length: int, hop_length: int, sr: float ) -> npt.NDArray[np.float64]: ... def blocks_to_time( blocks: _ScalarOrSequence[_IntLike_co], *, block_length: int, hop_length: int, sr: float, ) -> np.float64 | npt.NDArray[np.float64]: """Convert block indices to time (in seconds) Parameters ---------- blocks : np.ndarray Block index or array of block indices block_length : int > 0 The number of frames per block hop_length : int > 0 The number of samples to advance between frames sr : int > 0 The sampling rate (samples per second) Returns ------- times : np.ndarray [shape=samples.shape] The time index or indices (in seconds) corresponding to the beginning of each provided block. Note that these correspond to the time of the *first* sample in each block, and are not frame-centered. See Also -------- blocks_to_frames blocks_to_samples Examples -------- Get time indices for each block in a stream >>> filename = librosa.ex('brahms') >>> sr = librosa.get_samplerate(filename) >>> stream = librosa.stream(filename, block_length=16, ... frame_length=2048, hop_length=512) >>> for n, y in enumerate(stream): ... n_time = librosa.blocks_to_time(n, block_length=16, ... hop_length=512, sr=sr) """ samples = blocks_to_samples( blocks, block_length=block_length, hop_length=hop_length ) return samples_to_time(samples, sr=sr) @overload def note_to_hz(note: str, *, round_midi: bool = ...) -> np.float64: ... @overload def note_to_hz( note: _IterableLike[str], *, round_midi: bool = ... ) -> _Array1D[np.float64]: ... @overload def note_to_hz( note: Iterable[str], *, round_midi: bool = ... ) -> np.float64 | _Array1D[np.float64]: ... def note_to_hz( note: str | _IterableLike[str] | Iterable[str], *, round_midi: bool = False ) -> np.float64 | _Array1D[np.float64]: """Convert one or more note names to frequency (Hz) Examples -------- >>> # Get the frequency of a note >>> librosa.note_to_hz('C') array([ 16.352]) >>> # Or multiple notes >>> librosa.note_to_hz(['A3', 'A4', 'A5']) array([ 220., 440., 880.]) >>> # Notes with tuning deviations >>> librosa.note_to_hz(['C2-32', 'C2']) array([ 64.209, 65.406]) >>> # Or discarding tuning deviations >>> librosa.note_to_hz(['C2-32', 'C2'], round_midi=True) array([ 65.406, 65.406]) Parameters ---------- note : str or iterable of str One or more note names to convert round_midi : bool (default=False) If ``True``, quantize the note to the nearest MIDI pitch before conversion. If ``False``, allow for cent deviations in converting to Hz. Returns ------- frequencies : number or np.ndarray [shape=(len(note),)] Array of frequencies (in Hz) corresponding to ``note`` See Also -------- midi_to_hz note_to_midi hz_to_note """ return midi_to_hz(note_to_midi(note, round_midi=round_midi)) @overload def note_to_midi(note: str, *, round_midi: bool = ...) -> float: ... @overload def note_to_midi( note: _IterableLike[str], *, round_midi: bool = ... ) -> _Array1D[np.float64]: ... @overload def note_to_midi( note: Iterable[str], *, round_midi: bool = ... ) -> float | _Array1D[np.float64]: ... def note_to_midi( note: str | Iterable[str], *, round_midi: bool = True ) -> float | _Array1D[np.float64]: """Convert one or more spelled notes to MIDI number(s). Notes may be spelled out with optional accidentals or octave numbers. The leading note name is case-insensitive. The empty string ``""`` maps to `np.nan`. Sharps are indicated with ``#``, flats may be indicated with ``!`` or ``b``. Parameters ---------- note : str or iterable of str One or more note names. round_midi : bool - If ``True``, midi numbers are rounded to the nearest integer. - If ``False``, allow fractional midi numbers. Returns ------- midi : float or np.array Midi note numbers corresponding to inputs. Raises ------ ParameterError If the input is not in valid note format See Also -------- midi_to_note note_to_hz Examples -------- >>> librosa.note_to_midi('C') 12 >>> librosa.note_to_midi('C#3') 49 >>> librosa.note_to_midi('C♯3') # Using Unicode sharp 49 >>> librosa.note_to_midi('Câ™­3') # Using Unicode flat 47 >>> librosa.note_to_midi('f4') 65 >>> librosa.note_to_midi('Bb-1') 10 >>> librosa.note_to_midi('A!8') 116 >>> librosa.note_to_midi('Gð�„ª6') # Double-sharp 93 >>> librosa.note_to_midi('Bð�„«6') # Double-flat 93 >>> librosa.note_to_midi('Câ™­ð�„«5') # Triple-flats also work 69 >>> # Lists of notes also work >>> librosa.note_to_midi(['C', 'E', 'G']) array([12, 16, 19]) """ if not isinstance(note, str): return np.array([note_to_midi(n, round_midi=round_midi) for n in note]) if note == "": return np.nan pitch_map: dict[str, int] = { "C": 0, "D": 2, "E": 4, "F": 5, "G": 7, "A": 9, "B": 11, } acc_map: dict[str, int] = { "#": 1, "": 0, "b": -1, "!": -1, "♯": 1, "ð�„ª": 2, "â™­": -1, "ð�„«": -2, "â™®": 0, } match = notation.NOTE_RE.match(note) if not match: raise ParameterError(f"Improper note format: {note:s}") pitch = match.group("note").upper() offset = np.sum([acc_map[o] for o in match.group("accidental")]) octave = match.group("octave") cents = match.group("cents") if not octave: octave = 0 else: octave = int(octave) if not cents: cents = 0 else: cents = int(cents) * 1e-2 note_value: float = 12 * (octave + 1) + pitch_map[pitch] + offset + cents if round_midi: return int(np.round(note_value)) else: return note_value @overload def midi_to_note( midi: _FloatLike_co, *, octave: bool = ..., cents: bool = ..., key: str = ..., unicode: bool = ..., ) -> str: ... @overload def midi_to_note( midi: _SequenceLike[_FloatLike_co], *, octave: bool = ..., cents: bool = ..., key: str = ..., unicode: bool = ..., ) -> npt.NDArray[np.str_]: ... @vectorize(excluded=["octave", "cents", "key", "unicode"]) def midi_to_note( midi: _ScalarOrSequence[_FloatLike_co], *, octave: bool = True, cents: bool = False, key: str = "C:maj", unicode: bool = True, ) -> str | npt.NDArray[np.str_]: """Convert one or more MIDI numbers to note strings. MIDI numbers will be rounded to the nearest integer. Non-finite values (NaN, inf, -inf) will return an empty string. Notes will be of the format 'C0', 'C♯0', 'D0', ... Examples -------- >>> librosa.midi_to_note(0) 'C-1' >>> librosa.midi_to_note(37) 'C♯2' >>> librosa.midi_to_note(37, unicode=False) 'C#2' >>> librosa.midi_to_note(-2) 'A♯-2' >>> librosa.midi_to_note(104.7) 'A7' >>> librosa.midi_to_note(104.7, cents=True) 'A7-30' >>> librosa.midi_to_note(np.arange(12, 24))) array(['C0', 'C♯0', 'D0', 'D♯0', 'E0', 'F0', 'F♯0', 'G0', 'G♯0', 'A0', 'A♯0', 'B0'], dtype='>> librosa.midi_to_note(range(12, 24), key='F:min') array(['C0', 'Dâ™­0', 'D0', 'Eâ™­0', 'E0', 'F0', 'Gâ™­0', 'G0', 'Aâ™­0', 'A0', 'Bâ™­0', 'B0'], dtype=' np.float64: ... @overload def midi_to_hz(notes: _FloatLike_co) -> np.floating: ... @overload def midi_to_hz(notes: Sequence[float]) -> _Array1D[np.float64]: ... @overload def midi_to_hz(notes: _SequenceLike[_FloatLike_co]) -> np.ndarray: ... def midi_to_hz(notes: _ScalarOrSequence[_FloatLike_co]) -> np.floating | np.ndarray: """Get the frequency (Hz) of MIDI note(s) Examples -------- >>> librosa.midi_to_hz(36) 65.406 >>> librosa.midi_to_hz(np.arange(36, 48)) array([ 65.406, 69.296, 73.416, 77.782, 82.407, 87.307, 92.499, 97.999, 103.826, 110. , 116.541, 123.471]) Parameters ---------- notes : int or np.ndarray [shape=(n,), dtype=int] midi number(s) of the note(s) Returns ------- frequency : number or np.ndarray [shape=(n,), dtype=float] frequency (frequencies) of ``notes`` in Hz See Also -------- hz_to_midi note_to_hz """ return 440.0 * (2.0 ** ((np.asanyarray(notes)[()] - 69.0) / 12.0)) @overload def hz_to_midi(frequencies: float) -> np.float64: ... @overload def hz_to_midi(frequencies: _FloatLike_co) -> np.floating: ... @overload def hz_to_midi(frequencies: Sequence[float]) -> _Array1D[np.float64]: ... @overload def hz_to_midi(frequencies: _SequenceLike[_FloatLike_co]) -> np.ndarray: ... def hz_to_midi(frequencies: _ScalarOrSequence[_FloatLike_co]) -> np.floating | np.ndarray: """Get MIDI note number(s) for given frequencies Examples -------- >>> librosa.hz_to_midi(60) 34.506 >>> librosa.hz_to_midi([110, 220, 440]) array([ 45., 57., 69.]) Parameters ---------- frequencies : float or np.ndarray [shape=(n,), dtype=float] frequencies to convert Returns ------- note_nums : number or np.ndarray [shape=(n,), dtype=float] MIDI notes to ``frequencies`` See Also -------- midi_to_hz note_to_midi hz_to_note """ midi: np.ndarray = 12 * (np.log2(np.asanyarray(frequencies)) - np.log2(440.0)) + 69 return midi[()] @overload def hz_to_note(frequencies: _FloatLike_co, **kwargs: Any) -> str: ... @overload def hz_to_note( frequencies: _SequenceLike[_FloatLike_co], **kwargs: Any ) -> npt.NDArray[np.str_]: ... def hz_to_note( frequencies: _ScalarOrSequence[_FloatLike_co], **kwargs: Any ) -> str | npt.NDArray[np.str_]: """Convert one or more frequencies (in Hz) to the nearest note names. Parameters ---------- frequencies : float or iterable of float Input frequencies, specified in Hz **kwargs : additional keyword arguments Arguments passed through to `midi_to_note` Returns ------- notes : str or np.ndarray of str ``notes[i]`` is the closest note name to ``frequency[i]`` (or ``frequency`` if the input is scalar) See Also -------- hz_to_midi midi_to_note note_to_hz Examples -------- Get a single note name for a frequency >>> librosa.hz_to_note(440.0) 'A5' Get multiple notes with cent deviation >>> librosa.hz_to_note([32, 64], cents=True) ['C1-38', 'C2-38'] Get multiple notes, but suppress octave labels >>> librosa.hz_to_note(440.0 * (2.0 ** np.linspace(0, 1, 12)), ... octave=False) ['A', 'A#', 'B', 'C', 'C#', 'D', 'E', 'F', 'F#', 'G', 'G#', 'A'] """ return midi_to_note(hz_to_midi(frequencies), **kwargs) @overload def hz_to_mel(frequencies: float, *, htk: bool = ...) -> np.float64: ... @overload def hz_to_mel(frequencies: _FloatLike_co, *, htk: bool = ...) -> np.floating: ... @overload def hz_to_mel(frequencies: Sequence[float], *, htk: bool = ...) -> _Array1D[np.float64]: ... @overload def hz_to_mel( frequencies: _SequenceLike[_FloatLike_co], *, htk: bool = ... ) -> np.ndarray: ... def hz_to_mel( frequencies: _ScalarOrSequence[_FloatLike_co], *, htk: bool = False ) -> np.floating | np.ndarray: """Convert Hz to Mels Examples -------- >>> librosa.hz_to_mel(60) 0.9 >>> librosa.hz_to_mel([110, 220, 440]) array([ 1.65, 3.3 , 6.6 ]) Parameters ---------- frequencies : number or np.ndarray [shape=(n,)] , float scalar or array of frequencies htk : bool use HTK formula instead of Slaney Returns ------- mels : number or np.ndarray [shape=(n,)] input frequencies in Mels See Also -------- mel_to_hz """ frequencies = np.asanyarray(frequencies)[()] if htk: mels: np.floating | np.ndarray = 2595.0 * np.log10(1.0 + frequencies / 700.0) return mels # Fill in the linear part f_min = 0.0 f_sp = 200.0 / 3 mels = (frequencies - f_min) / f_sp # Fill in the log-scale part min_log_hz = 1000.0 # beginning of log region (Hz) min_log_mel = (min_log_hz - f_min) / f_sp # same (Mels) logstep = np.log(6.4) / 27.0 # step size for log region if frequencies.ndim: # If we have array data, vectorize log_t = frequencies >= min_log_hz mels[log_t] = min_log_mel + np.log(frequencies[log_t] / min_log_hz) / logstep elif frequencies >= min_log_hz: # If we have scalar data, heck directly mels = min_log_mel + np.log(frequencies / min_log_hz) / logstep return mels @overload def mel_to_hz(mels: float, *, htk: bool = ...) -> np.float64: ... @overload def mel_to_hz(mels: _FloatLike_co, *, htk: bool = ...) -> np.floating: ... @overload def mel_to_hz(mels: Sequence[float], *, htk: bool = ...) -> _Array1D[np.float64]: ... @overload def mel_to_hz(mels: _SequenceLike[_FloatLike_co], *, htk: bool = ...) -> np.ndarray: ... def mel_to_hz( mels: _ScalarOrSequence[_FloatLike_co], *, htk: bool = False ) -> np.floating | np.ndarray: """Convert mel bin numbers to frequencies Examples -------- >>> librosa.mel_to_hz(3) 200. >>> librosa.mel_to_hz([1,2,3,4,5]) array([ 66.667, 133.333, 200. , 266.667, 333.333]) Parameters ---------- mels : np.ndarray [shape=(n,)], float mel bins to convert htk : bool use HTK formula instead of Slaney Returns ------- frequencies : np.ndarray [shape=(n,)] input mels in Hz See Also -------- hz_to_mel """ mels = np.asanyarray(mels)[()] if htk: return 700.0 * (10.0 ** (mels / 2595.0) - 1.0) # Fill in the linear scale f_min = 0.0 f_sp = 200.0 / 3 freqs = f_min + f_sp * mels # And now the nonlinear scale min_log_hz = 1000.0 # beginning of log region (Hz) min_log_mel = (min_log_hz - f_min) / f_sp # same (Mels) logstep = np.log(6.4) / 27.0 # step size for log region if mels.ndim: # If we have vector data, vectorize log_t = mels >= min_log_mel freqs[log_t] = min_log_hz * np.exp(logstep * (mels[log_t] - min_log_mel)) elif mels >= min_log_mel: # If we have scalar data, check directly freqs = min_log_hz * np.exp(logstep * (mels - min_log_mel)) return freqs @overload def hz_to_octs( frequencies: float, *, tuning: float = ..., bins_per_octave: int = ... ) -> np.float64: ... @overload def hz_to_octs( frequencies: _FloatLike_co, *, tuning: float = ..., bins_per_octave: int = ... ) -> np.floating: ... @overload def hz_to_octs( frequencies: Sequence[float], *, tuning: float = ..., bins_per_octave: int = ..., ) -> _Array1D[np.float64]: ... @overload def hz_to_octs( frequencies: _SequenceLike[_FloatLike_co], *, tuning: float = ..., bins_per_octave: int = ..., ) -> np.ndarray: ... def hz_to_octs( frequencies: _ScalarOrSequence[_FloatLike_co], *, tuning: float = 0.0, bins_per_octave: int = 12, ) -> np.floating | np.ndarray: """Convert frequencies (Hz) to (fractional) octave numbers. Examples -------- >>> librosa.hz_to_octs(440.0) 4. >>> librosa.hz_to_octs([32, 64, 128, 256]) array([ 0.219, 1.219, 2.219, 3.219]) Parameters ---------- frequencies : number >0 or np.ndarray [shape=(n,)] or float scalar or vector of frequencies tuning : float Tuning deviation from A440 in (fractional) bins per octave. bins_per_octave : int > 0 Number of bins per octave. Returns ------- octaves : number or np.ndarray [shape=(n,)] octave number for each frequency See Also -------- octs_to_hz """ A440 = 440.0 * 2.0 ** (tuning / bins_per_octave) octs: np.ndarray = np.log2(np.asanyarray(frequencies) / (float(A440) / 16)) return octs[()] @overload def octs_to_hz( octs: float, *, tuning: float = ..., bins_per_octave: int = ... ) -> np.float64: ... @overload def octs_to_hz( octs: _FloatLike_co, *, tuning: float = ..., bins_per_octave: int = ... ) -> np.floating: ... @overload def octs_to_hz( octs: Sequence[float], *, tuning: float = ..., bins_per_octave: int = ... ) -> _Array1D[np.float64]: ... @overload def octs_to_hz( octs: _SequenceLike[_FloatLike_co], *, tuning: float = ..., bins_per_octave: int = ..., ) -> np.ndarray: ... def octs_to_hz( octs: _ScalarOrSequence[_FloatLike_co], *, tuning: float = 0.0, bins_per_octave: int = 12, ) -> np.floating | np.ndarray: """Convert octaves numbers to frequencies. Octaves are counted relative to A. Examples -------- >>> librosa.octs_to_hz(1) 55. >>> librosa.octs_to_hz([-2, -1, 0, 1, 2]) array([ 6.875, 13.75 , 27.5 , 55. , 110. ]) Parameters ---------- octs : np.ndarray [shape=(n,)] or float octave number for each frequency tuning : float Tuning deviation from A440 in (fractional) bins per octave. bins_per_octave : int > 0 Number of bins per octave. Returns ------- frequencies : number or np.ndarray [shape=(n,)] scalar or vector of frequencies See Also -------- hz_to_octs """ A440 = 440.0 * 2.0 ** (tuning / bins_per_octave) return (float(A440) / 16) * (2.0 ** np.asanyarray(octs)[()]) @overload def A4_to_tuning(A4: float, *, bins_per_octave: int = ...) -> np.float64: ... @overload def A4_to_tuning(A4: _FloatLike_co, *, bins_per_octave: int = ...) -> np.floating: ... @overload def A4_to_tuning( A4: Sequence[float], *, bins_per_octave: int = ... ) -> _Array1D[np.float64]: ... @overload def A4_to_tuning( A4: _SequenceLike[_FloatLike_co], *, bins_per_octave: int = ... ) -> np.ndarray: ... def A4_to_tuning( A4: _ScalarOrSequence[_FloatLike_co], *, bins_per_octave: int = 12 ) -> np.floating | np.ndarray: """Convert a reference pitch frequency (e.g., ``A4=435``) to a tuning estimation, in fractions of a bin per octave. This is useful for determining the tuning deviation relative to A440 of a given frequency, assuming equal temperament. By default, 12 bins per octave are used. This method is the inverse of `tuning_to_A4`. Examples -------- The base case of this method in which A440 yields 0 tuning offset from itself. >>> librosa.A4_to_tuning(440.0) 0. Convert a non-A440 frequency to a tuning offset relative to A440 using the default of 12 bins per octave. >>> librosa.A4_to_tuning(432.0) -0.318 Convert two reference pitch frequencies to corresponding tuning estimations at once, but using 24 bins per octave. >>> librosa.A4_to_tuning([440.0, 444.0], bins_per_octave=24) array([ 0., 0.313 ]) Parameters ---------- A4 : float or np.ndarray [shape=(n,), dtype=float] Reference frequency(s) corresponding to A4. bins_per_octave : int > 0 Number of bins per octave. Returns ------- tuning : float or np.ndarray [shape=(n,), dtype=float] Tuning deviation from A440 in (fractional) bins per octave. See Also -------- tuning_to_A4 """ tuning: np.ndarray = bins_per_octave * (np.log2(np.asanyarray(A4)) - np.log2(440.0)) return tuning[()] @overload def tuning_to_A4(tuning: float, *, bins_per_octave: int = ...) -> np.float64: ... @overload def tuning_to_A4(tuning: _FloatLike_co, *, bins_per_octave: int = ...) -> np.floating: ... @overload def tuning_to_A4( tuning: Sequence[float], *, bins_per_octave: int = ... ) -> _Array1D[np.float64]: ... @overload def tuning_to_A4( tuning: _SequenceLike[_FloatLike_co], *, bins_per_octave: int = ... ) -> np.ndarray: ... def tuning_to_A4( tuning: _ScalarOrSequence[_FloatLike_co], *, bins_per_octave: int = 12 ) -> np.floating | np.ndarray: """Convert a tuning deviation (from 0) to a reference pitch frequency relative to A440. This is useful if you are working in a non-A440 tuning system to determine the reference pitch frequency given a tuning offset and assuming equal temperament. By default, 12 bins per octave are used. This method is the inverse of `A4_to_tuning`. Examples -------- The base case of this method in which a tuning deviation of 0 gets us to our A440 reference pitch. >>> librosa.tuning_to_A4(0.0) 440. Convert a nonzero tuning offset to its reference pitch frequency. >>> librosa.tuning_to_A4(-0.318) 431.992 Convert 3 tuning deviations at once to respective reference pitch frequencies, using 36 bins per octave. >>> librosa.tuning_to_A4([0.1, 0.2, -0.1], bins_per_octave=36) array([ 440.848, 441.698 439.154]) Parameters ---------- tuning : float or np.ndarray [shape=(n,), dtype=float] Tuning deviation from A440 in fractional bins per octave. bins_per_octave : int > 0 Number of bins per octave. Returns ------- A4 : float or np.ndarray [shape=(n,), dtype=float] Reference frequency corresponding to A4. See Also -------- A4_to_tuning """ return 440.0 * 2.0 ** (np.asanyarray(tuning)[()] / bins_per_octave) def fft_frequencies(*, sr: float = 22050, n_fft: int = 2048) -> _Array1D[np.float64]: """Alternative interface for `np.fft.rfftfreq` Parameters ---------- sr : number > 0 [scalar] Audio sampling rate n_fft : int > 0 [scalar] length of the FFT frame Returns ------- freqs : np.ndarray [shape=(1 + n_fft/2,)] Frequencies ``(0, sr/n_fft, 2*sr/n_fft, ..., sr/2)`` Examples -------- >>> librosa.fft_frequencies(sr=22050, n_fft=16) array([ 0. , 1378.125, 2756.25 , 4134.375, 5512.5 , 6890.625, 8268.75 , 9646.875, 11025. ]) """ # the return dtype was unnecessarily broad in the numpy<2.5 dtype stubs return cast("_Array1D[np.float64]", np.fft.rfftfreq(n=n_fft, d=1.0 / sr)) def cqt_frequencies( n_bins: int, *, fmin: float, bins_per_octave: int = 12, tuning: float = 0.0 ) -> _Array1D[np.float64]: """Compute the center frequencies of Constant-Q bins. Examples -------- >>> # Get the CQT frequencies for 24 notes, starting at C2 >>> librosa.cqt_frequencies(24, fmin=librosa.note_to_hz('C2')) array([ 65.406, 69.296, 73.416, 77.782, 82.407, 87.307, 92.499, 97.999, 103.826, 110. , 116.541, 123.471, 130.813, 138.591, 146.832, 155.563, 164.814, 174.614, 184.997, 195.998, 207.652, 220. , 233.082, 246.942]) Parameters ---------- n_bins : int > 0 [scalar] Number of constant-Q bins fmin : float > 0 [scalar] Minimum frequency bins_per_octave : int > 0 [scalar] Number of bins per octave tuning : float Deviation from A440 tuning in fractional bins Returns ------- frequencies : np.ndarray [shape=(n_bins,)] Center frequency for each CQT bin """ correction: float = 2.0 ** (float(tuning) / bins_per_octave) frequencies: np.ndarray = 2.0 ** ( np.arange(0, n_bins, dtype=float) / bins_per_octave ) return correction * fmin * frequencies def mel_frequencies( n_mels: int = 128, *, fmin: float = 0.0, fmax: float = 11025.0, htk: bool = False ) -> _Array1D[np.float64]: """Compute an array of acoustic frequencies tuned to the mel scale. The mel scale is a quasi-logarithmic function of acoustic frequency designed such that perceptually similar pitch intervals (e.g. octaves) appear equal in width over the full hearing range. Because the definition of the mel scale is conditioned by a finite number of subjective psychoacoustical experiments, several implementations coexist in the audio signal processing literature [#]_. By default, librosa replicates the behavior of the well-established MATLAB Auditory Toolbox of Slaney [#]_. According to this default implementation, the conversion from Hertz to mel is linear below 1 kHz and logarithmic above 1 kHz. Another available implementation replicates the Hidden Markov Toolkit [#]_ (HTK) according to the following formula:: mel = 2595.0 * np.log10(1.0 + f / 700.0). The choice of implementation is determined by the ``htk`` keyword argument: setting ``htk=False`` leads to the Auditory toolbox implementation, whereas setting it ``htk=True`` leads to the HTK implementation. .. [#] Umesh, S., Cohen, L., & Nelson, D. Fitting the mel scale. In Proc. International Conference on Acoustics, Speech, and Signal Processing (ICASSP), vol. 1, pp. 217-220, 1998. .. [#] Slaney, M. Auditory Toolbox: A MATLAB Toolbox for Auditory Modeling Work. Technical Report, version 2, Interval Research Corporation, 1998. .. [#] Young, S., Evermann, G., Gales, M., Hain, T., Kershaw, D., Liu, X., Moore, G., Odell, J., Ollason, D., Povey, D., Valtchev, V., & Woodland, P. The HTK book, version 3.4. Cambridge University, March 2009. See Also -------- hz_to_mel mel_to_hz librosa.feature.melspectrogram librosa.feature.mfcc Parameters ---------- n_mels : int > 0 [scalar] Number of mel bins. fmin : float >= 0 [scalar] Minimum frequency (Hz). fmax : float >= 0 [scalar] Maximum frequency (Hz). htk : bool If True, use HTK formula to convert Hz to mel. Otherwise (False), use Slaney's Auditory Toolbox. Returns ------- bin_frequencies : ndarray [shape=(n_mels,)] Vector of ``n_mels`` frequencies in Hz which are uniformly spaced on the Mel axis. Examples -------- >>> librosa.mel_frequencies(n_mels=40) array([ 0. , 85.317, 170.635, 255.952, 341.269, 426.586, 511.904, 597.221, 682.538, 767.855, 853.173, 938.49 , 1024.856, 1119.114, 1222.042, 1334.436, 1457.167, 1591.187, 1737.532, 1897.337, 2071.84 , 2262.393, 2470.47 , 2697.686, 2945.799, 3216.731, 3512.582, 3835.643, 4188.417, 4573.636, 4994.285, 5453.621, 5955.205, 6502.92 , 7101.009, 7754.107, 8467.272, 9246.028, 10096.408, 11025. ]) """ # 'Center freqs' of mel bands - uniformly spaced between limits min_mel = hz_to_mel(fmin, htk=htk) max_mel = hz_to_mel(fmax, htk=htk) mels = np.linspace(min_mel, max_mel, n_mels) return mel_to_hz(mels, htk=htk) def tempo_frequencies( n_bins: int, *, hop_length: int = 512, sr: float = 22050 ) -> _Array1D[np.float64]: """Compute the frequencies (in beats per minute) corresponding to an onset auto-correlation or tempogram matrix. Parameters ---------- n_bins : int > 0 The number of lag bins hop_length : int > 0 The number of samples between each bin sr : number > 0 The audio sampling rate Returns ------- bin_frequencies : ndarray [shape=(n_bins,)] vector of bin frequencies measured in BPM. .. note:: ``bin_frequencies[0] = +np.inf`` corresponds to 0-lag Examples -------- Get the tempo frequencies corresponding to a 384-bin (8-second) tempogram >>> librosa.tempo_frequencies(384, sr=22050) array([ inf, 2583.984, 1291.992, ..., 6.782, 6.764, 6.747]) """ bin_frequencies = np.zeros(int(n_bins), dtype=np.float64) bin_frequencies[0] = np.inf bin_frequencies[1:] = 60.0 * sr / (hop_length * np.arange(1.0, n_bins)) return bin_frequencies def fourier_tempo_frequencies( *, sr: float = 22050, win_length: int = 384, hop_length: int = 512 ) -> _Array1D[np.float64]: """Compute the frequencies (in beats per minute) corresponding to a Fourier tempogram matrix. Parameters ---------- sr : number > 0 The audio sampling rate win_length : int > 0 The number of frames per analysis window hop_length : int > 0 The number of samples between each bin Returns ------- bin_frequencies : ndarray [shape=(win_length // 2 + 1 ,)] vector of bin frequencies measured in BPM. Examples -------- Get the tempo frequencies corresponding to a 384-bin (8-second) tempogram >>> librosa.fourier_tempo_frequencies(win_length=384, sr=22050) array([ 0. , 0.117, 0.234, ..., 22.266, 22.383, 22.5 ]) """ # sr / hop_length gets the frame rate # multiplying by 60 turns frames / sec into frames / minute return fft_frequencies(sr=sr * 60 / float(hop_length), n_fft=win_length) @overload def A_weighting(frequencies: float, *, min_db: float | None = ...) -> np.float64: ... @overload def A_weighting(frequencies: _FloatLike_co, *, min_db: float | None = ...) -> np.floating: ... @overload def A_weighting( frequencies: Sequence[float], *, min_db: float | None = ... ) -> _Array1D[np.float64]: ... @overload def A_weighting( frequencies: _SequenceLike[_FloatLike_co], *, min_db: float | None = ... ) -> np.ndarray: ... def A_weighting( frequencies: _ScalarOrSequence[_FloatLike_co], *, min_db: float | None = -80.0 ) -> np.floating | np.ndarray: """Compute the A-weighting of a set of frequencies. Parameters ---------- frequencies : scalar or np.ndarray [shape=(n,)] One or more frequencies (in Hz) min_db : float [scalar] or None Clip weights below this threshold. If `None`, no clipping is performed. Returns ------- A_weighting : scalar or np.ndarray [shape=(n,)] ``A_weighting[i]`` is the A-weighting of ``frequencies[i]`` See Also -------- perceptual_weighting frequency_weighting multi_frequency_weighting B_weighting C_weighting D_weighting Examples -------- Get the A-weighting for CQT frequencies >>> import matplotlib.pyplot as plt >>> freqs = librosa.cqt_frequencies(n_bins=108, fmin=librosa.note_to_hz('C1')) >>> weights = librosa.A_weighting(freqs) >>> fig, ax = plt.subplots() >>> ax.plot(freqs, weights) >>> ax.set(xlabel='Frequency (Hz)', ... ylabel='Weighting (log10)', ... title='A-Weighting of CQT frequencies') """ f_sq = np.asanyarray(frequencies)[()] ** 2.0 const = np.array([12194.217, 20.598997, 107.65265, 737.86223]) ** 2.0 weights: np.ndarray with np.errstate(divide="ignore"): # Temporarily ignore div-by-zero warnings since min_db might clean # them up later weights = 2.0 + 20.0 * ( np.log10(const[0]) + 2 * np.log10(f_sq) - np.log10(f_sq + const[0]) - np.log10(f_sq + const[1]) - 0.5 * np.log10(f_sq + const[2]) - 0.5 * np.log10(f_sq + const[3]) ) if min_db is None: if not np.all(np.isfinite(weights)): # If we have any non-finite values, then we should return # an array of NaNs warnings.warn( "A-weighting returned non-finite values. " "This is likely due to low (~0 Hz) frequencies.", stacklevel=2, ) return weights else: weights = np.maximum(min_db, weights) return weights @overload def B_weighting(frequencies: float, *, min_db: float | None = ...) -> np.float64: ... @overload def B_weighting(frequencies: _FloatLike_co, *, min_db: float | None = ...) -> np.floating: ... @overload def B_weighting( frequencies: Sequence[float], *, min_db: float | None = ... ) -> _Array1D[np.float64]: ... @overload def B_weighting( frequencies: _SequenceLike[_FloatLike_co], *, min_db: float | None = ... ) -> np.ndarray: ... def B_weighting( frequencies: _ScalarOrSequence[_FloatLike_co], *, min_db: float | None = -80.0 ) -> np.floating | np.ndarray: """Compute the B-weighting of a set of frequencies. Parameters ---------- frequencies : scalar or np.ndarray [shape=(n,)] One or more frequencies (in Hz) min_db : float [scalar] or None Clip weights below this threshold. If `None`, no clipping is performed. Returns ------- B_weighting : scalar or np.ndarray [shape=(n,)] ``B_weighting[i]`` is the B-weighting of ``frequencies[i]`` See Also -------- perceptual_weighting frequency_weighting multi_frequency_weighting A_weighting C_weighting D_weighting Examples -------- Get the B-weighting for CQT frequencies >>> import matplotlib.pyplot as plt >>> freqs = librosa.cqt_frequencies(n_bins=108, fmin=librosa.note_to_hz('C1')) >>> weights = librosa.B_weighting(freqs) >>> fig, ax = plt.subplots() >>> ax.plot(freqs, weights) >>> ax.set(xlabel='Frequency (Hz)', ... ylabel='Weighting (log10)', ... title='B-Weighting of CQT frequencies') """ f_sq = np.asanyarray(frequencies)[()] ** 2.0 const = np.array([12194.217, 20.598997, 158.48932]) ** 2.0 weights: np.ndarray with np.errstate(divide="ignore"): # Temporarily ignore div-by-zero warnings since min_db might clean # them up later weights = 0.17 + 20.0 * ( np.log10(const[0]) + 1.5 * np.log10(f_sq) - np.log10(f_sq + const[0]) - np.log10(f_sq + const[1]) - 0.5 * np.log10(f_sq + const[2]) ) if min_db is None: if not np.all(np.isfinite(weights)): # If we have any non-finite values, then we should return # an array of NaNs warnings.warn( "B-weighting returned non-finite values. " "This is likely due to low (~0 Hz) frequencies.", stacklevel=2, ) return weights else: weights = np.maximum(min_db, weights) return weights @overload def C_weighting(frequencies: float, *, min_db: float | None = ...) -> np.float64: ... @overload def C_weighting(frequencies: _FloatLike_co, *, min_db: float | None = ...) -> np.floating: ... @overload def C_weighting( frequencies: Sequence[float], *, min_db: float | None = ... ) -> _Array1D[np.float64]: ... @overload def C_weighting( frequencies: _SequenceLike[_FloatLike_co], *, min_db: float | None = ... ) -> np.ndarray: ... def C_weighting( frequencies: _ScalarOrSequence[_FloatLike_co], *, min_db: float | None = -80.0 ) -> np.floating | np.ndarray: """Compute the C-weighting of a set of frequencies. Parameters ---------- frequencies : scalar or np.ndarray [shape=(n,)] One or more frequencies (in Hz) min_db : float [scalar] or None Clip weights below this threshold. If `None`, no clipping is performed. Returns ------- C_weighting : scalar or np.ndarray [shape=(n,)] ``C_weighting[i]`` is the C-weighting of ``frequencies[i]`` See Also -------- perceptual_weighting frequency_weighting multi_frequency_weighting A_weighting B_weighting D_weighting Examples -------- Get the C-weighting for CQT frequencies >>> import matplotlib.pyplot as plt >>> freqs = librosa.cqt_frequencies(n_bins=108, fmin=librosa.note_to_hz('C1')) >>> weights = librosa.C_weighting(freqs) >>> fig, ax = plt.subplots() >>> ax.plot(freqs, weights) >>> ax.set(xlabel='Frequency (Hz)', ylabel='Weighting (log10)', ... title='C-Weighting of CQT frequencies') """ f_sq = np.asanyarray(frequencies)[()] ** 2.0 const = np.array([12194.217, 20.598997]) ** 2.0 weights: np.ndarray with np.errstate(divide="ignore"): weights = 0.062 + 20.0 * ( np.log10(const[0]) + np.log10(f_sq) - np.log10(f_sq + const[0]) - np.log10(f_sq + const[1]) ) if min_db is None: if not np.all(np.isfinite(weights)): # If we have any non-finite values, then we should return # an array of NaNs warnings.warn( "C-weighting returned non-finite values. " "This is likely due to low (~0 Hz) frequencies.", stacklevel=2, ) return weights else: weights = np.maximum(min_db, weights) return weights @overload def D_weighting(frequencies: float, *, min_db: float | None = ...) -> np.float64: ... @overload def D_weighting(frequencies: _FloatLike_co, *, min_db: float | None = ...) -> np.floating: ... @overload def D_weighting( frequencies: Sequence[float], *, min_db: float | None = ... ) -> _Array1D[np.float64]: ... @overload def D_weighting( frequencies: _SequenceLike[_FloatLike_co], *, min_db: float | None = ... ) -> np.ndarray: ... def D_weighting( frequencies: _ScalarOrSequence[_FloatLike_co], *, min_db: float | None = -80.0 ) -> np.floating | np.ndarray: """Compute the D-weighting of a set of frequencies. Parameters ---------- frequencies : scalar or np.ndarray [shape=(n,)] One or more frequencies (in Hz) min_db : float [scalar] or None Clip weights below this threshold. If `None`, no clipping is performed. Returns ------- D_weighting : scalar or np.ndarray [shape=(n,)] ``D_weighting[i]`` is the D-weighting of ``frequencies[i]`` See Also -------- perceptual_weighting frequency_weighting multi_frequency_weighting A_weighting B_weighting C_weighting Examples -------- Get the D-weighting for CQT frequencies >>> import matplotlib.pyplot as plt >>> freqs = librosa.cqt_frequencies(n_bins=108, fmin=librosa.note_to_hz('C1')) >>> weights = librosa.D_weighting(freqs) >>> fig, ax = plt.subplots() >>> ax.plot(freqs, weights) >>> ax.set(xlabel='Frequency (Hz)', ylabel='Weighting (log10)', ... title='D-Weighting of CQT frequencies') """ f_sq = np.asanyarray(frequencies)[()] ** 2.0 const = np.array([8.3046305e-3, 1018.7, 1039.6, 3136.5, 3424, 282.7, 1160]) ** 2.0 weights: np.ndarray with np.errstate(divide="ignore"): weights = 20.0 * ( 0.5 * np.log10(f_sq) - np.log10(const[0]) + 0.5 * ( +np.log10((const[1] - f_sq) ** 2 + const[2] * f_sq) - np.log10((const[3] - f_sq) ** 2 + const[4] * f_sq) - np.log10(const[5] + f_sq) - np.log10(const[6] + f_sq) ) ) if min_db is None: if not np.all(np.isfinite(weights)): # If we have any non-finite values, then we should return # an array of NaNs warnings.warn( "D-weighting returned non-finite values. " "This is likely due to low (~0 Hz) frequencies.", stacklevel=2, ) return weights else: weights = np.maximum(min_db, weights) return weights def Z_weighting( frequencies: Sized, *, min_db: float | None = None ) -> _Array1D[np.float64]: """Apply no weighting curve (aka Z-weighting). This function behaves similarly to `A_weighting`, `B_weighting`, etc., but all frequencies are equally weighted. An optional threshold `min_db` can still be used to clip energies. Parameters ---------- frequencies : scalar or np.ndarray [shape=(n,)] One or more frequencies (in Hz) min_db : float [scalar] or None Clip weights below this threshold. If `None`, no clipping is performed. Returns ------- Z_weighting : scalar or np.ndarray [shape=(n,)] ``Z_weighting[i]`` is the Z-weighting of ``frequencies[i]`` See Also -------- perceptual_weighting frequency_weighting multi_frequency_weighting A_weighting B_weighting C_weighting D_weighting """ weights: np.ndarray = np.zeros(len(frequencies)) if min_db is None: return weights else: weights = np.maximum(min_db, weights) return weights WEIGHTING_FUNCTIONS: dict[ str | None, Callable[..., np.floating | np.ndarray] ] = { "A": A_weighting, "B": B_weighting, "C": C_weighting, "D": D_weighting, "Z": Z_weighting, None: Z_weighting, } @overload def frequency_weighting( frequencies: float, *, kind: str = ..., **kwargs: Any ) -> np.float64: ... @overload def frequency_weighting( frequencies: _FloatLike_co, *, kind: str = ..., **kwargs: Any ) -> np.floating: ... @overload def frequency_weighting( frequencies: Sequence[float], *, kind: str = ..., **kwargs: Any ) -> _Array1D[np.float64]: ... @overload def frequency_weighting( frequencies: _SequenceLike[_FloatLike_co], *, kind: str = ..., **kwargs: Any ) -> np.ndarray: ... def frequency_weighting( frequencies: _ScalarOrSequence[_FloatLike_co], *, kind: str = "A", **kwargs: Any ) -> np.floating | np.ndarray: """Compute the weighting of a set of frequencies. Parameters ---------- frequencies : scalar or np.ndarray [shape=(n,)] One or more frequencies (in Hz) kind : str in The weighting kind. e.g. `'A'`, `'B'`, `'C'`, `'D'`, `'Z'` **kwargs Additional keyword arguments to A_weighting, B_weighting, etc. Returns ------- weighting : scalar or np.ndarray [shape=(n,)] ``weighting[i]`` is the weighting of ``frequencies[i]`` See Also -------- perceptual_weighting multi_frequency_weighting A_weighting B_weighting C_weighting D_weighting Examples -------- Get the A-weighting for CQT frequencies >>> import matplotlib.pyplot as plt >>> freqs = librosa.cqt_frequencies(n_bins=108, fmin=librosa.note_to_hz('C1')) >>> weights = librosa.frequency_weighting(freqs, kind='A') >>> fig, ax = plt.subplots() >>> ax.plot(freqs, weights) >>> ax.set(xlabel='Frequency (Hz)', ylabel='Weighting (log10)', ... title='A-Weighting of CQT frequencies') """ if isinstance(kind, str): kind = kind.upper() return WEIGHTING_FUNCTIONS[kind](frequencies, **kwargs) def multi_frequency_weighting( frequencies: _ScalarOrSequence[_FloatLike_co], *, kinds: Iterable[str] = "ZAC", **kwargs: Any, ) -> np.ndarray: """Compute multiple weightings of a set of frequencies. Parameters ---------- frequencies : scalar or np.ndarray [shape=(n,)] One or more frequencies (in Hz) kinds : list or tuple or str An iterable of weighting kinds. e.g. `('Z', 'B')`, `'ZAD'`, `'C'` **kwargs Additional arguments to pass to the weighting function. Returns ------- weighting : scalar or np.ndarray [shape=(len(kinds), n)] ``weighting[i, j]`` is the weighting of ``frequencies[j]`` using the curve determined by ``kinds[i]``. See Also -------- perceptual_weighting frequency_weighting A_weighting B_weighting C_weighting D_weighting Examples -------- Get the A, B, C, D, and Z weightings for CQT frequencies >>> import matplotlib.pyplot as plt >>> freqs = librosa.cqt_frequencies(n_bins=108, fmin=librosa.note_to_hz('C1')) >>> weightings = 'ABCDZ' >>> weights = librosa.multi_frequency_weighting(freqs, kinds=weightings) >>> fig, ax = plt.subplots() >>> for label, w in zip(weightings, weights): ... ax.plot(freqs, w, label=label) >>> ax.set(xlabel='Frequency (Hz)', ylabel='Weighting (log10)', ... title='Weightings of CQT frequencies') >>> ax.legend() """ return np.stack( [frequency_weighting(frequencies, kind=k, **kwargs) for k in kinds], axis=0 ) def times_like( X: np.ndarray | float, *, sr: float = 22050, hop_length: int = 512, n_fft: int | None = None, axis: int = -1, ) -> np.ndarray: """Return an array of time values to match the time axis from a feature matrix. Parameters ---------- X : np.ndarray or scalar - If ndarray, X is a feature matrix, e.g. STFT, chromagram, or mel spectrogram. - If scalar, X represents the number of frames. sr : number > 0 [scalar] audio sampling rate hop_length : int > 0 [scalar] number of samples between successive frames n_fft : None or int > 0 [scalar] Optional: length of the FFT frame. If given, time conversion will include an offset of ``n_fft // 2`` to counteract windowing effects when using a non-centered STFT. axis : int [scalar] The axis representing the time axis of X. By default, the last axis (-1) is taken. Returns ------- times : np.ndarray [shape=(n,)] ndarray of times (in seconds) corresponding to each frame of X. See Also -------- samples_like : Return an array of sample indices to match the time axis from a feature matrix. Examples -------- Provide a feature matrix input: >>> y, sr = librosa.loadx('trumpet') >>> D = librosa.stft(y) >>> times = librosa.times_like(D, sr=sr) >>> times array([0. , 0.023, ..., 5.294, 5.317]) Provide a scalar input: >>> n_frames = 2647 >>> times = librosa.times_like(n_frames, sr=sr) >>> times array([ 0.00000000e+00, 2.32199546e-02, 4.64399093e-02, ..., 6.13935601e+01, 6.14167800e+01, 6.14400000e+01]) """ samples = samples_like(X, hop_length=hop_length, n_fft=n_fft, axis=axis) time: np.ndarray = samples_to_time(samples, sr=sr) return time def samples_like( X: np.ndarray | float, *, hop_length: int = 512, n_fft: int | None = None, axis: int = -1, ) -> np.ndarray: """Return an array of sample indices to match the time axis from a feature matrix. Parameters ---------- X : np.ndarray or scalar - If ndarray, X is a feature matrix, e.g. STFT, chromagram, or mel spectrogram. - If scalar, X represents the number of frames. hop_length : int > 0 [scalar] number of samples between successive frames n_fft : None or int > 0 [scalar] Optional: length of the FFT frame. If given, time conversion will include an offset of ``n_fft // 2`` to counteract windowing effects when using a non-centered STFT. axis : int [scalar] The axis representing the time axis of ``X``. By default, the last axis (-1) is taken. Returns ------- samples : np.ndarray [shape=(n,)] ndarray of sample indices corresponding to each frame of ``X``. See Also -------- times_like : Return an array of time values to match the time axis from a feature matrix. Examples -------- Provide a feature matrix input: >>> y, sr = librosa.loadx('trumpet') >>> X = librosa.stft(y) >>> samples = librosa.samples_like(X) >>> samples array([ 0, 512, ..., 116736, 117248]) Provide a scalar input: >>> n_frames = 2647 >>> samples = librosa.samples_like(n_frames) >>> samples array([ 0, 512, 1024, ..., 1353728, 1354240, 1354752]) """ # suppress type checks because mypy does not understand isscalar if np.isscalar(X): frames = np.arange(X) # type: ignore else: frames = np.arange(X.shape[axis]) # type: ignore return frames_to_samples(frames, hop_length=hop_length, n_fft=n_fft) @overload def midi_to_svara_h( midi: _FloatLike_co, *, Sa: _FloatLike_co, abbr: bool = ..., octave: bool = ..., unicode: bool = ..., ) -> str: ... @overload def midi_to_svara_h( midi: np.ndarray, *, Sa: _FloatLike_co, abbr: bool = ..., octave: bool = ..., unicode: bool = ..., ) -> npt.NDArray[np.str_]: ... @vectorize(excluded=["Sa", "abbr", "octave", "unicode"]) def midi_to_svara_h( midi: _FloatLike_co | np.ndarray, *, Sa: _FloatLike_co, abbr: bool = True, octave: bool = True, unicode: bool = True, ) -> str | npt.NDArray[np.str_]: """Convert MIDI numbers to Hindustani svara Non-finite midi numbers (e.g., NaN, inf) will return an empty string. Parameters ---------- midi : numeric or np.ndarray The MIDI number or numbers to convert Sa : number > 0 MIDI number of the reference Sa. abbr : bool If `True` (default) return abbreviated names ('S', 'r', 'R', 'g', 'G', ...) If `False`, return long-form names ('Sa', 're', 'Re', 'ga', 'Ga', ...) octave : bool If `True`, decorate svara in neighboring octaves with over- or under-dots. If `False`, ignore octave height information. unicode : bool If `True`, use unicode symbols to decorate octave information. If `False`, use low-order ASCII (' and ,) for octave decorations. This only takes effect if `octave=True`. Returns ------- svara : str or np.ndarray of str The svara corresponding to the given MIDI number(s) See Also -------- hz_to_svara_h note_to_svara_h midi_to_svara_c midi_to_note Examples -------- Convert a single midi number: >>> librosa.midi_to_svara_h(65, Sa=60) 'm' The first three svara with Sa at midi number 60: >>> librosa.midi_to_svara_h([60, 61, 62], Sa=60) array(['S', 'r', 'R'], dtype='>> librosa.midi_to_svara_h([60, 61, 62], Sa=67) array(['mÌ£', 'MÌ£', 'PÌ£'], dtype='>> librosa.midi_to_svara_h([60, 61, 62], Sa=67, unicode=False) array(['m,', 'M,', 'P,'], dtype='>> librosa.midi_to_svara_h([72, 73, 74], Sa=60, abbr=False) array(['Ṡa', 'ṙe', 'Ṙe'], dtype=' svara_num >= 12: if unicode: svara = svara[0] + "\u0307" + svara[1:] else: svara += "'" elif -12 <= svara_num < 0: if unicode: svara = svara[0] + "\u0323" + svara[1:] else: svara += "," return svara @overload def hz_to_svara_h( frequencies: _FloatLike_co, *, Sa: _FloatLike_co, abbr: bool = ..., octave: bool = ..., unicode: bool = ..., ) -> str: ... @overload def hz_to_svara_h( frequencies: _SequenceLike[_FloatLike_co], *, Sa: _FloatLike_co, abbr: bool = ..., octave: bool = ..., unicode: bool = ..., ) -> npt.NDArray[np.str_]: ... def hz_to_svara_h( frequencies: _ScalarOrSequence[_FloatLike_co], *, Sa: _FloatLike_co, abbr: bool = True, octave: bool = True, unicode: bool = True, ) -> str | npt.NDArray[np.str_]: """Convert frequencies (in Hz) to Hindustani svara Note that this conversion assumes 12-tone equal temperament. Parameters ---------- frequencies : positive number or np.ndarray The frequencies (in Hz) to convert Sa : positive number Frequency (in Hz) of the reference Sa. abbr : bool If `True` (default) return abbreviated names ('S', 'r', 'R', 'g', 'G', ...) If `False`, return long-form names ('Sa', 're', 'Re', 'ga', 'Ga', ...) octave : bool If `True`, decorate svara in neighboring octaves with over- or under-dots. If `False`, ignore octave height information. unicode : bool If `True`, use unicode symbols to decorate octave information. If `False`, use low-order ASCII (' and ,) for octave decorations. This only takes effect if `octave=True`. Returns ------- svara : str or np.ndarray of str The svara corresponding to the given frequency/frequencies See Also -------- midi_to_svara_h note_to_svara_h hz_to_svara_c hz_to_note Examples -------- Convert Sa in three octaves: >>> librosa.hz_to_svara_h([261/2, 261, 261*2], Sa=261) ['SÌ£', 'S', 'Ṡ'] Convert one octave worth of frequencies with full names: >>> freqs = librosa.cqt_frequencies(n_bins=12, fmin=261) >>> librosa.hz_to_svara_h(freqs, Sa=freqs[0], abbr=False) ['Sa', 're', 'Re', 'ga', 'Ga', 'ma', 'Ma', 'Pa', 'dha', 'Dha', 'ni', 'Ni'] """ midis = hz_to_midi(frequencies) return midi_to_svara_h( midis, Sa=hz_to_midi(Sa), abbr=abbr, octave=octave, unicode=unicode ) @overload def note_to_svara_h( notes: str, *, Sa: str, abbr: bool = ..., octave: bool = ..., unicode: bool = ... ) -> str: ... @overload def note_to_svara_h( notes: _IterableLike[str], *, Sa: str, abbr: bool = ..., octave: bool = ..., unicode: bool = ..., ) -> npt.NDArray[np.str_]: ... def note_to_svara_h( notes: str | _IterableLike[str], *, Sa: str, abbr: bool = True, octave: bool = True, unicode: bool = True, ) -> str | npt.NDArray[np.str_]: """Convert western notes to Hindustani svara Note that this conversion assumes 12-tone equal temperament. Parameters ---------- notes : str or iterable of str Notes to convert (e.g., `'C#'` or `['C4', 'Db4', 'D4']` Sa : str Note corresponding to Sa (e.g., `'C'` or `'C5'`). If no octave information is provided, it will default to octave 0 (``C0`` ~= 16 Hz) abbr : bool If `True` (default) return abbreviated names ('S', 'r', 'R', 'g', 'G', ...) If `False`, return long-form names ('Sa', 're', 'Re', 'ga', 'Ga', ...) octave : bool If `True`, decorate svara in neighboring octaves with over- or under-dots. If `False`, ignore octave height information. unicode : bool If `True`, use unicode symbols to decorate octave information. If `False`, use low-order ASCII (' and ,) for octave decorations. This only takes effect if `octave=True`. Returns ------- svara : str or np.ndarray of str The svara corresponding to the given notes See Also -------- midi_to_svara_h hz_to_svara_h note_to_svara_c note_to_midi note_to_hz Examples -------- >>> librosa.note_to_svara_h(['C4', 'G4', 'C5', 'G5'], Sa='C5') ['SÌ£', 'PÌ£', 'S', 'P'] """ midis = note_to_midi(notes, round_midi=False) return midi_to_svara_h( midis, Sa=note_to_midi(Sa), abbr=abbr, octave=octave, unicode=unicode ) @overload def midi_to_svara_c( midi: _FloatLike_co, *, Sa: _FloatLike_co, mela: int | str, abbr: bool = ..., octave: bool = ..., unicode: bool = ..., ) -> str: ... @overload def midi_to_svara_c( midi: np.ndarray, *, Sa: _FloatLike_co, mela: int | str, abbr: bool = ..., octave: bool = ..., unicode: bool = ..., ) -> npt.NDArray[np.str_]: ... @vectorize(excluded=["Sa", "mela", "abbr", "octave", "unicode"]) # type: ignore def midi_to_svara_c( midi: float | np.ndarray, *, Sa: _FloatLike_co, mela: int | str, abbr: bool = True, octave: bool = True, unicode: bool = True, ) -> str | npt.NDArray[np.str_]: """Convert MIDI numbers to Carnatic svara within a given melakarta raga Non-finite midi numbers (e.g., NaN, inf) will return an empty string. Parameters ---------- midi : numeric The MIDI numbers to convert Sa : number > 0 MIDI number of the reference Sa. Default: 60 (261.6 Hz, `C4`) mela : int or str The name or index of the melakarta raga abbr : bool If `True` (default) return abbreviated names ('S', 'R1', 'R2', 'G1', 'G2', ...) If `False`, return long-form names ('Sa', 'Ri1', 'Ri2', 'Ga1', 'Ga2', ...) octave : bool If `True`, decorate svara in neighboring octaves with over- or under-dots. If `False`, ignore octave height information. unicode : bool If `True`, use unicode symbols to decorate octave information and subscript numbers. If `False`, use low-order ASCII (' and ,) for octave decorations. Returns ------- svara : str or np.ndarray of str The svara corresponding to the given MIDI number(s) See Also -------- hz_to_svara_c note_to_svara_c mela_to_degrees mela_to_svara list_mela """ if not np.isfinite(midi): return "" svara_num = int(np.round(midi - Sa)) svara_map = notation.mela_to_svara(mela, abbr=abbr, unicode=unicode) svara = svara_map[svara_num % 12] if octave: if 24 > svara_num >= 12: if unicode: svara = svara[0] + "\u0307" + svara[1:] else: svara += "'" elif -12 <= svara_num < 0: if unicode: svara = svara[0] + "\u0323" + svara[1:] else: svara += "," return svara @overload def hz_to_svara_c( frequencies: float, *, Sa: float, mela: int | str, abbr: bool = ..., octave: bool = ..., unicode: bool = ..., ) -> str: ... @overload def hz_to_svara_c( frequencies: np.ndarray, *, Sa: float, mela: int | str, abbr: bool = ..., octave: bool = ..., unicode: bool = ..., ) -> npt.NDArray[np.str_]: ... def hz_to_svara_c( frequencies: float | np.ndarray, *, Sa: float, mela: int | str, abbr: bool = True, octave: bool = True, unicode: bool = True, ) -> str | npt.NDArray[np.str_]: """Convert frequencies (in Hz) to Carnatic svara Note that this conversion assumes 12-tone equal temperament. Parameters ---------- frequencies : positive number or np.ndarray The frequencies (in Hz) to convert Sa : positive number Frequency (in Hz) of the reference Sa. mela : int [1, 72] or str The melakarta raga to use. abbr : bool If `True` (default) return abbreviated names ('S', 'R1', 'R2', 'G1', 'G2', ...) If `False`, return long-form names ('Sa', 'Ri1', 'Ri2', 'Ga1', 'Ga2', ...) octave : bool If `True`, decorate svara in neighboring octaves with over- or under-dots. If `False`, ignore octave height information. unicode : bool If `True`, use unicode symbols to decorate octave information. If `False`, use low-order ASCII (' and ,) for octave decorations. This only takes effect if `octave=True`. Returns ------- svara : str or np.ndarray of str The svara corresponding to the given frequency/frequencies See Also -------- note_to_svara_c midi_to_svara_c hz_to_svara_h hz_to_note list_mela Examples -------- Convert Sa in three octaves: >>> librosa.hz_to_svara_c([261/2, 261, 261*2], Sa=261, mela='kanakangi') ['SÌ£', 'S', 'Ṡ'] Convert one octave worth of frequencies using melakarta #36: >>> freqs = librosa.cqt_frequencies(n_bins=12, fmin=261) >>> librosa.hz_to_svara_c(freqs, Sa=freqs[0], mela=36) ['S', 'Râ‚�', 'Râ‚‚', 'R₃', 'G₃', 'Mâ‚�', 'Mâ‚‚', 'P', 'Dâ‚�', 'Dâ‚‚', 'D₃', 'N₃'] """ midis = hz_to_midi(frequencies) return midi_to_svara_c( midis, Sa=hz_to_midi(Sa), mela=mela, abbr=abbr, octave=octave, unicode=unicode ) @overload def note_to_svara_c( notes: str, *, Sa: str, mela: str | int, abbr: bool = ..., octave: bool = ..., unicode: bool = ..., ) -> str: ... @overload def note_to_svara_c( notes: _IterableLike[str], *, Sa: str, mela: str | int, abbr: bool = ..., octave: bool = ..., unicode: bool = ..., ) -> npt.NDArray[np.str_]: ... def note_to_svara_c( notes: str | _IterableLike[str], *, Sa: str, mela: str | int, abbr: bool = True, octave: bool = True, unicode: bool = True, ) -> str | npt.NDArray[np.str_]: """Convert western notes to Carnatic svara Note that this conversion assumes 12-tone equal temperament. Parameters ---------- notes : str or iterable of str Notes to convert (e.g., `'C#'` or `['C4', 'Db4', 'D4']` Sa : str Note corresponding to Sa (e.g., `'C'` or `'C5'`). If no octave information is provided, it will default to octave 0 (``C0`` ~= 16 Hz) mela : str or int [1, 72] Melakarta raga name or index abbr : bool If `True` (default) return abbreviated names ('S', 'R1', 'R2', 'G1', 'G2', ...) If `False`, return long-form names ('Sa', 'Ri1', 'Ri2', 'Ga1', 'Ga2', ...) octave : bool If `True`, decorate svara in neighboring octaves with over- or under-dots. If `False`, ignore octave height information. unicode : bool If `True`, use unicode symbols to decorate octave information. If `False`, use low-order ASCII (' and ,) for octave decorations. This only takes effect if `octave=True`. Returns ------- svara : str or np.ndarray of str The svara corresponding to the given notes See Also -------- midi_to_svara_c hz_to_svara_c note_to_svara_h note_to_midi note_to_hz list_mela Examples -------- >>> librosa.note_to_svara_h(['C4', 'G4', 'C5', 'D5', 'G5'], Sa='C5', mela=1) ['SÌ£', 'PÌ£', 'S', 'Gâ‚�', 'P'] """ midis = note_to_midi(notes, round_midi=False) return midi_to_svara_c( midis, Sa=note_to_midi(Sa), mela=mela, abbr=abbr, octave=octave, unicode=unicode ) @overload def hz_to_fjs( frequencies: _FloatLike_co, *, fmin: float | None = ..., unison: str | None = ..., unicode: bool = ..., ) -> str: ... @overload def hz_to_fjs( frequencies: _SequenceLike[_FloatLike_co], *, fmin: float | None = ..., unison: str | None = ..., unicode: bool = ..., ) -> npt.NDArray[np.str_]: ... def hz_to_fjs( frequencies: _ScalarOrSequence[_FloatLike_co], *, fmin: float | None = None, unison: str | None = None, unicode: bool = False, ) -> str | npt.NDArray[np.str_]: """Convert one or more frequencies (in Hz) from a just intonation scale to notes in FJS notation. Parameters ---------- frequencies : float or iterable of float Input frequencies, specified in Hz fmin : float (optional) The minimum frequency, corresponding to a unison note. If not provided, it will be inferred as `min(frequencies)` unison : str (optional) The name of the unison note. If not provided, it will be inferred as the scientific pitch notation name of `fmin`, that is, `hz_to_note(fmin)` unicode : bool If `True`, then unicode symbols are used for accidentals. If `False`, then low-order ASCII symbols are used for accidentals. Returns ------- notes : str or np.ndarray(dtype=str) ``notes[i]`` is the closest note name to ``frequency[i]`` (or ``frequency`` if the input is scalar) See Also -------- hz_to_note interval_to_fjs Examples -------- Get a single note name for a frequency, relative to A=55 Hz >>> librosa.hz_to_fjs(66, fmin=55, unicode=True) 'Câ‚…' Get notation for a 5-limit frequency set starting at A=55 >>> freqs = librosa.interval_frequencies(24, intervals="ji5", fmin=55) >>> freqs array([ 55. , 58.667, 61.875, 66. , 68.75 , 73.333, 77.344, 82.5 , 88. , 91.667, 99. , 103.125, 110. , 117.333, 123.75 , 132. , 137.5 , 146.667, 154.687, 165. , 176. , 183.333, 198. , 206.25 ]) >>> librosa.hz_to_fjs(freqs, unicode=True) array(['A', 'Bâ™­â‚…', 'B', 'Câ‚…', 'C♯â�µ', 'D', 'D♯â�µ', 'E', 'Fâ‚…', 'F♯â�µ', 'Gâ‚…', 'G♯â�µ', 'A', 'Bâ™­â‚…', 'B', 'Câ‚…', 'C♯â�µ', 'D', 'D♯â�µ', 'E', 'Fâ‚…', 'F♯â�µ', 'Gâ‚…', 'G♯â�µ'], dtype=' np.ndarray: """Harmonic salience function. Parameters ---------- S : np.ndarray [shape=(..., d, n)] input time frequency magnitude representation (e.g. STFT or CQT magnitudes). Must be real-valued and non-negative. freqs : np.ndarray, shape=(S.shape[axis]) or shape=S.shape The frequency values corresponding to S's elements along the chosen axis. Frequencies can also be time-varying, e.g. as computed by `reassigned_spectrogram`, in which case the shape should match ``S``. harmonics : list-like, non-negative Harmonics to include in salience computation. The first harmonic (1) corresponds to ``S`` itself. Values less than one (e.g., 1/2) correspond to sub-harmonics. weights : list-like The weight to apply to each harmonic in the summation. (default: uniform weights). Must be the same length as ``harmonics``. aggregate : function aggregation function (default: `np.average`) If ``aggregate=np.average``, then a weighted average is computed per-harmonic according to the specified weights. For all other aggregation functions, all harmonics are treated equally. filter_peaks : bool If true, returns harmonic summation only on frequencies of peak magnitude. Otherwise returns harmonic summation over the full spectrum. Defaults to True. fill_value : float The value to fill non-peaks in the output representation. (default: `np.nan`) Only used if ``filter_peaks == True``. kind : str Interpolation type for harmonic estimation. See `scipy.interpolate.interp1d`. axis : int The axis along which to compute harmonics Returns ------- S_sal : np.ndarray ``S_sal`` will have the same shape as ``S``, and measure the overall harmonic energy at each frequency. See Also -------- interp_harmonics Examples -------- >>> y, sr = librosa.loadx('trumpet', duration=3) >>> S = np.abs(librosa.stft(y)) >>> freqs = librosa.fft_frequencies(sr=sr) >>> harms = [1, 2, 3, 4] >>> weights = [1.0, 0.5, 0.33, 0.25] >>> S_sal = librosa.salience(S, freqs=freqs, harmonics=harms, weights=weights, fill_value=0) >>> print(S_sal.shape) (1025, 115) >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=2, sharex=True, sharey=True) >>> librosa.display.specshow(S, vscale='dBFS', ... sr=sr, y_axis='log', x_axis='time', ax=ax[0]) >>> ax[0].set(title='Magnitude spectrogram') >>> ax[0].label_outer() >>> img = librosa.display.specshow(S_sal, vscale='dBFS', ... sr=sr, y_axis='log', x_axis='time', ax=ax[1]) >>> ax[1].set(title='Salience spectrogram') >>> librosa.display.colorbar_db(img, ax=ax) """ if aggregate is None: aggregate = np.average if weights is None: weights = np.ones((len(harmonics),)) else: weights = np.array(weights, dtype=float) S_harm = interp_harmonics(S, freqs=freqs, harmonics=harmonics, kind=kind, axis=axis) S_sal: np.ndarray if aggregate is np.average: S_sal = aggregate(S_harm, axis=axis - 1, weights=weights) else: S_sal = aggregate(S_harm, axis=axis - 1) if filter_peaks: import scipy.signal S_peaks = scipy.signal.argrelmax(S, axis=axis) S_out = np.empty(S.shape) S_out.fill(fill_value) S_out[S_peaks] = S_sal[S_peaks] S_sal = S_out return S_sal def interp_harmonics( x: np.ndarray, *, freqs: np.ndarray, harmonics: ArrayLike, kind: _InterpKind = "linear", fill_value: float = 0, axis: int = -2, ) -> np.ndarray: """Compute the energy at harmonics of time-frequency representation. Given a frequency-based energy representation such as a spectrogram or tempogram, this function computes the energy at the chosen harmonics of the frequency axis. (See examples below.) The resulting harmonic array can then be used as input to a salience computation. Parameters ---------- x : np.ndarray The input energy freqs : np.ndarray, shape=(x.shape[axis]) or shape=x.shape The frequency values corresponding to x's elements along the chosen axis. Frequencies can also be time-varying, e.g. as computed by `reassigned_spectrogram`, in which case the shape should match ``x``. harmonics : list-like, non-negative Harmonics to compute as ``harmonics[i] * freqs``. The first harmonic (1) corresponds to ``freqs``. Values less than one (e.g., 1/2) correspond to sub-harmonics. kind : str Interpolation type. See `scipy.interpolate.interp1d`. fill_value : float The value to fill when extrapolating beyond the observed frequency range. axis : int The axis along which to compute harmonics Returns ------- x_harm : np.ndarray ``x_harm[i]`` will have the same shape as ``x``, and measure the energy at the ``harmonics[i]`` harmonic of each frequency. A new dimension indexing harmonics will be inserted immediately before ``axis``. See Also -------- scipy.interpolate.interp1d Examples -------- Estimate the harmonics of a time-averaged tempogram >>> y, sr = librosa.loadx('sweetwaltz') >>> # Compute the time-varying tempogram and average over time >>> tempi = np.mean(librosa.feature.tempogram(y=y, sr=sr), axis=1) >>> # We'll measure the first five harmonics >>> harmonics = [1, 2, 3, 4, 5] >>> f_tempo = librosa.tempo_frequencies(len(tempi), sr=sr) >>> # Build the harmonic tensor; we only have one axis here (tempo) >>> t_harmonics = librosa.interp_harmonics(tempi, freqs=f_tempo, harmonics=harmonics, axis=0) >>> print(t_harmonics.shape) (5, 384) >>> # And plot the results >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots() >>> librosa.display.specshow(t_harmonics, x_axis='tempo', sr=sr, ax=ax) >>> ax.set(yticks=np.arange(len(harmonics)), ... yticklabels=['{:.3g}'.format(_) for _ in harmonics], ... ylabel='Harmonic', xlabel='Tempo (BPM)') >>> plt.show() We can also compute frequency harmonics for spectrograms. To calculate sub-harmonic energy, use values < 1. >>> y, sr = librosa.loadx('trumpet', duration=3) >>> harmonics = [1./3, 1./2, 1, 2, 3, 4] >>> S = np.abs(librosa.stft(y)) >>> ref_value = np.max(S) # Common reference for dB scaling >>> fft_freqs = librosa.fft_frequencies(sr=sr) >>> S_harm = librosa.interp_harmonics(S, freqs=fft_freqs, harmonics=harmonics, axis=0) >>> print(S_harm.shape) (6, 1025, 646) >>> fig, ax = plt.subplots(nrows=3, ncols=2, sharex=True, sharey=True) >>> for i, _sh in enumerate(S_harm): ... img = librosa.display.specshow(_sh, vscale=f'dB[{ref_value}]', ... sr=sr, y_axis='log', x_axis='time', ... ax=ax.flat[i]) ... ax.flat[i].set(title='h={:.3g}'.format(harmonics[i])) ... ax.flat[i].label_outer() >>> librosa.display.colorbar_db(img, ax=ax) >>> plt.show() """ import scipy.interpolate if freqs.ndim == 1 and len(freqs) == x.shape[axis]: # Build the 1-D interpolator. # All frames have a common domain, so we only need one interpolator here. # First, verify that the input frequencies are unique if not is_unique(freqs, axis=0): warnings.warn( "Frequencies are not unique. This may produce incorrect harmonic interpolations.", stacklevel=2, ) f_interp = scipy.interpolate.interp1d( freqs, x, axis=axis, bounds_error=False, copy=False, kind=kind, fill_value=fill_value, ) # Set the interpolation points f_out = np.multiply.outer(harmonics, freqs) # Interpolate; suppress type checks return f_interp(f_out) elif freqs.shape == x.shape: if not np.all(is_unique(freqs, axis=axis)): warnings.warn( "Frequencies are not unique. This may produce incorrect harmonic interpolations.", stacklevel=2, ) # If we have time-varying frequencies, then it must match exactly the shape of the input # We'll define a frame-wise interpolator helper function that we will vectorize over # the entire input array def _f_interp(_a, _b): interp = scipy.interpolate.interp1d( _a, _b, bounds_error=False, copy=False, kind=kind, fill_value=fill_value ) return interp(np.multiply.outer(_a, harmonics)) # Signature is expanding frequency into a new dimension xfunc = np.vectorize(_f_interp, signature="(f),(f)->(f,h)") # Rotate the vectorizing axis to the tail so that we get parallelism over frames # Afterward, we're swapping (-1, axis-1) instead of (-1,axis) # because a new dimension has been inserted return ( # type: ignore xfunc(freqs.swapaxes(axis, -1), x.swapaxes(axis, -1)) .swapaxes( # Return the original target axis to its place -2, axis, ) .swapaxes( # Put the new harmonic axis directly in front of the target axis -1, axis - 1, ) ) else: raise ParameterError( f"freqs.shape={freqs.shape} is incompatible with input shape={x.shape}" ) def f0_harmonics( x: np.ndarray, *, f0: np.ndarray, freqs: np.ndarray, harmonics: ArrayLike, kind: _InterpKind = "linear", fill_value: float = 0, axis: int = -2, ) -> np.ndarray: """Compute the energy at selected harmonics of a time-varying fundamental frequency. This function can be used to reduce a `frequency * time` representation to a `harmonic * time` representation, effectively normalizing out for the fundamental frequency. The result can be used as a representation of timbre when f0 corresponds to pitch, or as a representation of rhythm when f0 corresponds to tempo. This function differs from `interp_harmonics`, which computes the harmonics of *all* frequencies. Parameters ---------- x : np.ndarray [shape=(..., frequencies, n)] The input array (e.g., STFT magnitudes) f0 : np.ndarray [shape=(..., n)] The fundamental frequency (f0) of each frame in the input Shape should match ``x.shape[-1]`` freqs : np.ndarray, shape=(x.shape[axis]) or shape=x.shape The frequency values corresponding to X's elements along the chosen axis. Frequencies can also be time-varying, e.g. as computed by `reassigned_spectrogram`, in which case the shape should match ``x``. harmonics : list-like, non-negative Harmonics to compute as ``harmonics[i] * f0`` Values less than one (e.g., 1/2) correspond to sub-harmonics. kind : str Interpolation type. See `scipy.interpolate.interp1d`. fill_value : float The value to fill when extrapolating beyond the observed frequency range. axis : int The axis corresponding to frequency in ``x`` Returns ------- f0_harm : np.ndarray [shape=(..., len(harmonics), n)] Interpolated energy at each specified harmonic of the fundamental frequency for each time step. See Also -------- interp_harmonics librosa.feature.tempogram_ratio Examples -------- This example estimates the fundamental (f0), and then extracts the first 12 harmonics >>> y, sr = librosa.loadx('trumpet') >>> f0, voicing, voicing_p = librosa.pyin(y=y, sr=sr, fmin=200, fmax=700) >>> S = np.abs(librosa.stft(y)) >>> freqs = librosa.fft_frequencies(sr=sr) >>> harmonics = np.arange(1, 13) >>> f0_harm = librosa.f0_harmonics(S, freqs=freqs, f0=f0, harmonics=harmonics) >>> import matplotlib.pyplot as plt >>> fig, ax =plt.subplots(nrows=2, sharex=True) >>> librosa.display.specshow(S, vscale='dBFS', ... x_axis='time', y_axis='log', ax=ax[0]) >>> times = librosa.times_like(f0) >>> highlight = librosa.display.highlight(ax=ax[0]) >>> for h in harmonics: ... ax[0].plot(times, h * f0, label=f"{h}*f0", path_effects=highlight) >>> ax[0].legend(ncols=4, loc='lower right') >>> ax[0].label_outer() >>> librosa.display.specshow(f0_harm, vscale='dBFS', ... x_axis='time', ax=ax[1]) >>> ax[1].set_yticks(harmonics-1) >>> ax[1].set_yticklabels(harmonics) >>> ax[1].set(ylabel='Harmonics') """ result: np.ndarray import scipy.interpolate if freqs.ndim == 1 and len(freqs) == x.shape[axis]: if not is_unique(freqs, axis=0): warnings.warn( "Frequencies are not unique. This may produce incorrect harmonic interpolations.", stacklevel=2, ) # We have a fixed frequency grid idx = np.isfinite(freqs) def _f_interps(data, f): interp = scipy.interpolate.interp1d( freqs[idx], data[idx], axis=0, bounds_error=False, copy=False, assume_sorted=False, kind=kind, fill_value=fill_value, ) return interp(f) xfunc = np.vectorize(_f_interps, signature="(f),(h)->(h)") result = xfunc(x.swapaxes(axis, -1), np.multiply.outer(f0, harmonics)).swapaxes( axis, -1 ) elif freqs.shape == x.shape: if not np.all(is_unique(freqs, axis=axis)): warnings.warn( "Frequencies are not unique. This may produce incorrect harmonic interpolations.", stacklevel=2, ) # We have a dynamic frequency grid, not so bad def _f_interpd(data, frequencies, f): idx = np.isfinite(frequencies) interp = scipy.interpolate.interp1d( frequencies[idx], data[idx], axis=0, bounds_error=False, copy=False, assume_sorted=False, kind=kind, fill_value=fill_value, ) return interp(f) xfunc = np.vectorize(_f_interpd, signature="(f),(f),(h)->(h)") result = xfunc( x.swapaxes(axis, -1), freqs.swapaxes(axis, -1), np.multiply.outer(f0, harmonics), ).swapaxes(axis, -1) else: raise ParameterError( f"freqs.shape={freqs.shape} is incompatible with input shape={x.shape}" ) result = np.nan_to_num(result, copy=False, nan=fill_value) return result ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2099245 librosa-1.0.0/librosa/core/intervals.msgpack0000644000175100017510000001215115236640653020564 0ustar00runnerrunnerÞMË?ð€Ë?ø‚ÿË?ò‚ýË?û‚üË?ô@‚úË?þ_ÿyÈBú‚ùË?öÇÿÞr¿‚÷Ë?ñÿæÕŒ�‚õË?ù¡`xÜ‚ôË?ó8À>-b‚ò Ë?üÕ�¥‡‚ñ Ë?õŸ×¡<%J‚ï Ë?ð7á¸íø‚í Ë?øSÓ›fù‚ì Ë?ò>Þ—Ðk¾‚êË?û^M]€ä—‚éË?ô†¹ÃÌî‚çË?þʤ‡3f‚æË?÷‘��#’‚äË?ñQ¬šþ¨‚âË?ùúƒô즇‚áË?ó{â.]á]‚ÐßË?ý9ÓËÄ�‚ÐÞË?õë__ (R‚ÐÜË?ðp‡‡H^>‚ÐÚË?ø¨ÊÄ´ÐW‚ÐÙË?ò~˜V£zÄ‚Ð×Ë?û½ãû½{ ‚ÐÖË?ôÎjüÎX‚ÐÔË?ÿ5¡l犂ÐÓË?÷h7÷¾‚ÐÑË?ñŽ)ùÎ�—‚ÐÏË?úU@%Nn‚ÐÎ Ë?ó¿ï9_J‚ÐÌ!Ë?ýŸç[ÄKõ‚ÐË"Ë?ö7íA·Zu‚ÐÉ#Ë?ð©òw�@Ý‚ÐÇ$Ë?øþë- $F‚ÐÆ%Ë?ò¿0aÇ›5‚ÐÄ&Ë?üÉã%Õ‚ÐÃ'Ë?õÒª\_‚ÐÁ(Ë?ÿ¢¡µÇÍŠ‚ÐÀ)Ë?÷¹ù‹q¸ª‚о*Ë?ñË{(•J€‚м+Ë?ú±8¼ßï¿‚л,Ë?ôêJŒM‚й-Ë?þ_oÒó‚и.Ë?ö…‡“Ý—ö‚ж/Ë?ðä%®æ1ù‚д0Ë?ùV8†YJõ‚г1Ë?óªêúµ=‚б2Ë?ü€ÿÚ@RÖ‚а3Ë?õ`¿]x�‚Ю4Ë?ð�†`Ô‚Ь5Ë?ø ×I'‘?‚Ы6Ë?ò ¢@1Hw‚Щ7Ë?ûrSÚr¨‚Ш8Ë?ôJÖˆ7q†‚Ц9Ë?þpAFmD‚Ð¥:Ë?öÔ0n\Ôí‚У;Ë?ñ$RÅŸ²‚С<Ë?ù®·`,‘‚Р=Ë?óCþ¬Bê‚О>Ë?üä�~d^‚Ð�?Ë?õ«iÛeìÄ‚Л@Ë?ð@�§¨P‚ЙAË?ø`×{|x!‚ИBË?òH¡Y�{–‚ЖCË?ûló±³l‚ЕDË?ô‘¶ éh‚ГEË?þÚ‘^‚Ð’FË?÷#íbs“‚Ð�GË?õUTûÚ×R‚ÿË?üqÇXÅÊ‚þË?òö„‹ë[-‚ýË?ùH± ¢@‚üË?ðÛ ¸l+‚ûË?öy€õ]厂 úË?ý÷VC´‚ ùË?óú9‚Í`‚ øË?ú¢÷±|å.‚÷Ë?ñÁúvSCt‚öË?÷­NL”-I‚õË?ÿ‘½aõ¾^‚ôË?õ Ó=)V;‚óË?üÅ�o‚òË?òµ-©†v§‚ñË?øñ‘Ýãu†‚ðË?ð¡ éBNY‚ïË?ö,”¢¹Ô‚îË?ý� $l‚íË?óµd0+@ö‚ ìË?úG/3Ê1è‚"ëË?ñ„ÊÕ{Ç÷‚#êË?÷[¸Ã*Œœ‚%éË?ÿ$öYŽÏ‚'èË?ôÃO?Ù3ã‚(çË?û¯H² y‚*æË?òt¸8Á®‚+åË?ø›Ÿ˜·‹‚-äË?ðg¿aª?‚.ãË?õßªŠ‚¥a‚0âË?ý*8¸®1ׂ2áË?óqzÌNøŒ‚3àË?ùì¤nò‚5ÐßË?ô‚þË?þƒýË?úª«%(®‚ÿË?ö€ƒûË?ù™™™™™š‚ÿË?ó333333ƒÿË?ñj‹�ƒÿÿË?üÌÌÌÌÌÍ‚ÿË?ñÇSó�ƒþË?öÁkÛmjƒþÿË?ù‚üË?òÀƒúË?ðª«%(®ƒýÿË?ðßÿyÈBúƒùË?üÿyÈBúƒùË?÷´%kÊSvƒýË?ö8㬠båƒÿþË?ôzáG®{‚þË?þ¸Që…¸ƒþË?ûN‚b™RƒÿþË?õ™™™™™šƒþÿË?þW:yx‘âƒýÿË?ò4V´A»ŒƒþþË?÷ =p£× ƒþË?ùOÿ¼ä!}ƒøË?ð333333ƒüÿË?ñG®záH‚þË?õÿ›V2<ƒ÷Ë?ø“t¼j~úƒýË?ðbMÒñ©ü‚ýË?õØgjrd¡ƒ ÿýË?òn—�Oß;ƒýË?ùë…¸QìƒÿþË?øEÈ–Ý&·ƒ ýþË?ô:'®š²žƒ üÿË?øLÌÌÌÌ̓ûÿË?ý ‰ãC0׃ þýË?û¥ãS÷ÎÙƒýË?òûÿŠ�:›ƒöË?óp£× =qƒýþË?ô¼j~ùÛ#ƒýË?ÿ£ÿï9_ƒöË?ò9™aÜ”ƒùÿË?÷pCÞƒƒøË?ñ”2TæâƒöË?ÿ@‚úË?ú]ÿÅG�NƒõË?ôÕTûÚ×RƒûÿË?ý¡/�ƒÜƒýË?ÿšÝœ'éSƒüË?÷ºÿ°NçŃôË?üyÿÖ”îƒõË?ûÇSó�ƒýþË?óÆ€”}ƒóË?ýLSâÖ$ƒöË?ó‡ÿÞr¿‚÷Ë?õøÿûÎBƒôË?óÀÊ` “ƒüË?ñÌ@JrêÙƒòË?ò„½3Ò•dƒþýË?ú ª}ík©ƒùÿË?ü‚þË?õƒüË?òI$kð#‚ÿË?ûm·(ÙºƒÿË?ô’H×à&F‚ÿË?øa†˜5ŒƒÿÿË?òª«%(®ƒÿÿË?ÿ€ƒûË?ñ€ƒûË?ú@„úË?ðÌÌÌÌÌÍ„þÿË?öffffff‚ÿË?÷UTûÚ×R„ýÿË?ýÝÞ7X[â„ÿÿË?øãް1‹“ƒþË?ó°CÞƒ„øË?ù333333„ýÿË?÷ŸÿyÈBúƒùË?ø€‚ûË?ò`†7½ƒùË?ÿqOÎtm„ÿþË?óé>›A:„þÿË?ðUTûÚ×RƒüÿË?ð—´ûß ƒýË?öÛmCÐ9iƒÿË?ýAÓª6ŸÏƒÿÿË?ðA þ;ƒþÿË?þyç¸ �é„ÿÿË?ñ$’¼/Æ—„ÿÿË?õñ_ˆü“d„ÿÿË?ó�8yÄ<„ÿÿÿË?ñë…¸QìƒþË?õßÿyÈBúƒùË?ý*«%(®„ûÿË?ðhd©ÍÄ„÷Ë?÷ä±$Йá„ÿþË?úáG®zá„þË?ó™™™™™šƒýÿË?þŸÿyÈBúƒùË?ôj«%(®„úÿË?öø!�ïA„÷Ë?ú"!ȧ¤„ÿÿÿË?ýffffff„üÿË?û�ÿ¼ä!}ƒøË?õÇSó�ƒþþË?ö‚ý Ë?÷EÐúX÷‚ ÿË?ú‚ý Ë?ó±:Õ¾ãÖ‚ ÿË?ð€ƒû Ë?ýUTûÚ×Rƒÿÿ Ë?ñt\þޗЃ ÿË?ÿÂÀð ƒÿ ÿË?ó€ƒû Ë?ñUTûÚ×Rƒþÿ Ë?ý‰ØÆÖǃ ÿË?úA¤zž+Ѓÿ ÿË?ñ‚üË?þ-èp—‚ÿË?ó‚üË?úò‡;Éê‚ÿË?ó@ƒú Ë?ô]ü›Çqƒ ÿË?û€ƒû Ë?ñ™™™™™šƒÿÿ Ë?ýFBÐ_ƒ ÿË?òž@ÈG,ƒÿ ÿË?ù€ƒûË?ö––åŠ2ôƒÿË?öª«%(®ƒþÿË?ôšõºƒÿÿË?÷‚üË?öBÇû¬´)‚ÿË?ùUTûÚ×RƒþÿË?ü€ƒûË?ñ÷y „™ƒÿÿË?ô5å)ºämƒÿË?øÀƒú Ë?óŽ8§ç:6‚þ Ë?ôŸÿyÈBú„ù Ë?òUTûÚ×R„üÿ Ë?úffffff„þÿ Ë?÷wwÐñõ{„ÿÿ Ë?üà†7½„ù Ë?ùª«%(®„üÿ Ë?ù$’¼/Æ—‚ÿ Ë?ú.Œ… ¾ƒ ÿË?ô¯ÖQ°Ì¼ƒþ ÿË?ódØý\·‘„ÿ ÿË?øÓd„„ÿÿ ÿË?õÑsûz_Bƒ ÿË?ûíb8Ú _Array1D[np.float64]: """Construct a set of frequencies from an interval set Parameters ---------- n_bins : int The number of frequencies to generate fmin : float > 0 The minimum frequency intervals : str or array of floats in [1, 2) If `str`, must be one of the following: - `'equal'` - equal temperament - `'pythagorean'` - Pythagorean intervals - `'ji3'` - 3-limit just intonation - `'ji5'` - 5-limit just intonation - `'ji7'` - 7-limit just intonation Otherwise, an array of intervals in the range [1, 2) can be provided. bins_per_octave : int > 0 If `intervals` is a string specification, how many bins to generate per octave. If `intervals` is an array, then this parameter is ignored. tuning : float Deviation from A440 tuning in fractional bins. This is only used when `intervals == 'equal'` sort : bool Sort the intervals in ascending order. Returns ------- frequencies : array of float The frequencies Examples -------- Generate two octaves of Pythagorean intervals starting at 55Hz >>> librosa.interval_frequencies(24, fmin=55, intervals="pythagorean", bins_per_octave=12) array([ 55. , 58.733, 61.875, 66.075, 69.609, 74.334, 78.311, 82.5 , 88.099, 92.812, 99.112, 104.414, 110. , 117.466, 123.75 , 132.149, 139.219, 148.668, 156.621, 165. , 176.199, 185.625, 198.224, 208.828]) Generate two octaves of 5-limit intervals starting at 55Hz >>> librosa.interval_frequencies(24, fmin=55, intervals="ji5", bins_per_octave=12) array([ 55. , 58.667, 61.875, 66. , 68.75 , 73.333, 77.344, 82.5 , 88. , 91.667, 99. , 103.125, 110. , 117.333, 123.75 , 132. , 137.5 , 146.667, 154.687, 165. , 176. , 183.333, 198. , 206.25 ]) Generate three octaves using only three intervals >>> intervals = [1, 4/3, 3/2] >>> librosa.interval_frequencies(9, fmin=55, intervals=intervals) array([ 55. , 73.333, 82.5 , 110. , 146.667, 165. , 220. , 293.333, 330. ]) """ if isinstance(intervals, str): if intervals == "equal": # Maybe include tuning here? ratios = 2.0 ** ( (tuning + np.arange(0, bins_per_octave, dtype=float)) / bins_per_octave ) elif intervals == "pythagorean": ratios = pythagorean_intervals(bins_per_octave=bins_per_octave, sort=sort) elif intervals == "ji3": ratios = plimit_intervals( primes=[3], bins_per_octave=bins_per_octave, sort=sort ) elif intervals == "ji5": ratios = plimit_intervals( primes=[3, 5], bins_per_octave=bins_per_octave, sort=sort ) elif intervals == "ji7": ratios = plimit_intervals( primes=[3, 5, 7], bins_per_octave=bins_per_octave, sort=sort ) else: ratios = np.array(intervals) bins_per_octave = len(ratios) # We have one octave of ratios, tile it up to however many we need # and trim back to the right number of bins n_octaves = np.ceil(n_bins / bins_per_octave) all_ratios = np.multiply.outer(2.0 ** np.arange(n_octaves), ratios).flatten()[ :n_bins ] if sort: all_ratios = np.sort(all_ratios) frequencies: np.ndarray = all_ratios * fmin return frequencies @overload def pythagorean_intervals( *, bins_per_octave: int = ..., sort: bool = ..., return_factors: Literal[False] = ... ) -> _Array1D[np.float64]: ... @overload def pythagorean_intervals( *, bins_per_octave: int = ..., sort: bool = ..., return_factors: Literal[True] ) -> list[dict[int, int]]: ... @cache(level=10) def pythagorean_intervals( *, bins_per_octave: int = 12, sort: bool = True, return_factors: bool = False ) -> _Array1D[np.float64] | list[dict[int, int]]: """Pythagorean intervals Intervals are constructed by stacking ratios of 3/2 (i.e., just perfect fifths) and folding down to a single octave:: 1, 3/2, 9/8, 27/16, 81/64, ... Note that this differs from 3-limit just intonation intervals in that Pythagorean intervals only use positive powers of 3 (ascending fifths) while 3-limit intervals use both positive and negative powers (descending fifths). Parameters ---------- bins_per_octave : int The number of intervals to generate sort : bool If `True` then intervals are returned in ascending order. If `False`, then intervals are returned in circle-of-fifths order. return_factors : bool If `True` then return a list of dictionaries encoding the prime factorization of each interval as `{2: p2, 3: p3}` (meaning `3**p3 * 2**p2`). If `False` (default), return intervals as an array of floating point numbers. Returns ------- intervals : np.ndarray or list of dictionaries The constructed interval set. All intervals are mapped to the range [1, 2). See Also -------- plimit_intervals Examples -------- Generate the first 12 intervals >>> librosa.pythagorean_intervals(bins_per_octave=12) array([1. , 1.067871, 1.125 , 1.201355, 1.265625, 1.351524, 1.423828, 1.5 , 1.601807, 1.6875 , 1.802032, 1.898437]) >>> # Compare to the 12-tone equal temperament intervals: >>> 2**(np.arange(12)/12) array([1. , 1.059463, 1.122462, 1.189207, 1.259921, 1.33484 , 1.414214, 1.498307, 1.587401, 1.681793, 1.781797, 1.887749]) Or the first 7, in circle-of-fifths order >>> librosa.pythagorean_intervals(bins_per_octave=7, sort=False) array([1. , 1.5 , 1.125 , 1.6875 , 1.265625, 1.898437, 1.423828]) Generate the first 7, in circle-of-fifths other and factored form >>> librosa.pythagorean_intervals(bins_per_octave=7, sort=False, return_factors=True) [ {2: 0, 3: 0}, {2: -1, 3: 1}, {2: -3, 3: 2}, {2: -4, 3: 3}, {2: -6, 3: 4}, {2: -7, 3: 5}, {2: -9, 3: 6} ] """ # Generate all powers of 3 in log space pow3 = np.arange(bins_per_octave) # Using modf here to quickly get the fractional part of the log, # accounting for whatever power of 2 is necessary to get 3**k # within the octave. log_ratios, pow2 = np.modf(pow3 * np.log2(3)) # If the fractional part is negative, add # one more power of two to get it into the range [0, 1). too_small = log_ratios < 0 log_ratios[too_small] += 1 pow2[too_small] += 1 # Convert powers of 2 to integer pow2 = pow2.astype(int) idx: Iterable[int] if sort: # Order the intervals idx = np.argsort(log_ratios) log_ratios = log_ratios[idx] else: # If not sorting, we'll take powers in order idx = range(bins_per_octave) if return_factors: return list({2: -pow2[i], 3: pow3[i]} for i in idx) ratios: np.ndarray = np.power(2, log_ratios) return ratios def __harmonic_distance(logs, a, b): """Compute the harmonic distance between ratios a and b. Harmonic distance is defined as `log2(a * b) - 2*log2(gcd(a, b))` [#]_. Here we are expressing a and b as prime factorization exponents, and the prime basis are provided in their log2 form. .. [#] Tenney, James. "On `Crystal Growth' in harmonic space (1993--1998)." Contemporary Music Review 27.1 (2008): 47-56. """ a = np.array(a) b = np.array(b) # numerator = positive exponents a_num = np.maximum(a, 0) # denominator = negative exponents a_den = a_num - a b_num = np.maximum(b, 0) b_den = b_num - b # log2(ab / gcd(a,b)**2) = log(a) + log(b) - 2 * log(gcd(a,b)) # gcd(a,b) for rationals: gcd(a_num, b_num) / lcm(a_den, b_den) # gcd = minimum(a_num, b_num) and lcm = maximum(a_den, b_den) gcd = np.minimum(a_num, b_num) - np.maximum(a_den, b_den) # Rounding this to 6 decimals to avoid floating point weirdness return np.around(logs.dot(a + b - 2 * gcd), 6) def _crystal_tie_break(a, b, logs): """Given two tuples of prime powers, break ties.""" return logs.dot(np.abs(a)) < logs.dot(np.abs(b)) @overload def plimit_intervals( *, primes: ArrayLike, bins_per_octave: int = ..., sort: bool = ..., return_factors: Literal[False] = ..., ) -> NDArray[np.float64]: ... @overload def plimit_intervals( *, primes: ArrayLike, bins_per_octave: int = ..., sort: bool = ..., return_factors: Literal[True], ) -> list[dict[int, int]]: ... @cache(level=10) def plimit_intervals( *, primes: ArrayLike, bins_per_octave: int = 12, sort: bool = True, return_factors: bool = False, ) -> NDArray[np.float64] | list[dict[int, int]]: """Construct p-limit intervals for a given set of prime factors. This function is based on the "harmonic crystal growth" algorithm of [#1]_ [#2]_. .. [#1] Tenney, James. "On 'Crystal Growth' in harmonic space (1993--1998)." Contemporary Music Review 27.1 (2008): 47--56. .. [#2] Sabat, Marc, and James Tenney. "Three crystal growth algorithms in 23-limit constrained harmonic space." Contemporary Music Review 27, no. 1 (2008): 57--78. Parameters ---------- primes : array of odd primes Which prime factors are to be used bins_per_octave : int The number of intervals to construct sort : bool If `True` then intervals are returned in ascending order. If `False` then intervals are returned in crystal growth order. return_factors : bool If `True` then return a list of dictionaries encoding the prime factorization of each interval as `{2: p2, 3: p3, ...}` (meaning `3**p3 * 2**p2`). If `False` (default), return intervals as an array of floating point numbers. Returns ------- intervals : np.ndarray or list of dictionaries The constructed interval set. All intervals are mapped to the range [1, 2). See Also -------- pythagorean_intervals Examples -------- Compare 3-limit tuning to Pythagorean tuning and 12-TET >>> librosa.plimit_intervals(primes=[3], bins_per_octave=12) array([1. , 1.05349794, 1.125 , 1.18518519, 1.265625 , 1.33333333, 1.40466392, 1.5 , 1.58024691, 1.6875 , 1.77777778, 1.8984375 ]) >>> # Pythagorean intervals: >>> librosa.pythagorean_intervals(bins_per_octave=12) array([1. , 1.06787109, 1.125 , 1.20135498, 1.265625 , 1.35152435, 1.42382812, 1.5 , 1.60180664, 1.6875 , 1.80203247, 1.8984375 ]) >>> # 12-TET intervals: >>> 2**(np.arange(12)/12) array([1. , 1.05946309, 1.12246205, 1.18920712, 1.25992105, 1.33483985, 1.41421356, 1.49830708, 1.58740105, 1.68179283, 1.78179744, 1.88774863]) Create a 7-bin, 5-limit interval set >>> librosa.plimit_intervals(primes=[3, 5], bins_per_octave=7) array([1. , 1.125 , 1.25 , 1.33333333, 1.5 , 1.66666667, 1.875 ]) The same example, but now in factored form >>> librosa.plimit_intervals(primes=[3, 5], bins_per_octave=7, ... return_factors=True) [ {}, {2: -3, 3: 2}, {2: -2, 5: 1}, {2: 2, 3: -1}, {2: -1, 3: 1}, {3: -1, 5: 1}, {2: -3, 3: 1, 5: 1} ] """ primes = np.atleast_1d(primes) logs = np.log2(primes, dtype=np.float64) # The seed set are primes and their reciprocals # These are the values that we can use to expand our # interval set. These are expressed in terms of the # prime factorization exponents seeds = [] for i in range(len(primes)): # Add the prime seed = [0] * len(primes) seed[i] = 1 seeds.append(tuple(seed)) # Add the inverse seed[i] = -1 seeds.append(tuple(seed)) # The frontier is the set of candidate intervals for inclusion frontier = seeds.copy() # The distances table will let us keep track of the harmonic # distances between all selected intervals distances = dict() # Initialize the interval set with the root (1) intervals = list() root = tuple([0] * len(primes)) intervals.append(root) while len(intervals) < bins_per_octave: # Find the element on the frontier that minimizes the total # harmonic distance to the existing set score = np.inf best_f = 0 for f, point in enumerate(frontier): # Compute harmonic distance (HD) to each selected interval HD = 0.0 for s in intervals: if (s, point) not in distances: distances[s, point] = __harmonic_distance(logs, point, s) distances[point, s] = distances[s, point] HD += distances[s, point] if HD < score or ( np.isclose(HD, score) and _crystal_tie_break(point, frontier[best_f], logs) ): score = HD best_f = f new_point = frontier.pop(best_f) intervals.append(new_point) for _ in seeds: new_seed = tuple(np.array(new_point) + np.array(_)) if new_seed not in intervals and new_seed not in frontier: frontier.append(new_seed) pows = np.array(list(intervals), dtype=float) log_ratios, pow2 = np.modf(pows.dot(logs)) # If the fractional part is negative, add # one more power of two to get it into the range [0, 1). too_small = log_ratios < 0 log_ratios[too_small] += 1 pow2[too_small] -= 1 # Convert powers of 2 to integer pow2 = pow2.astype(int) idx: Iterable[int] if sort: # Order the intervals idx = np.argsort(log_ratios) log_ratios = log_ratios[idx] else: # If not sorting, we'll take powers in order idx = range(bins_per_octave) if return_factors: # Collect the factorized intervals into a list factors = [] for i in idx: v = dict() if pow2[i] != 0: v[2] = -pow2[i] v.update({p: int(power) for p, power in zip(primes, pows[i], strict=False) if power != 0}) factors.append(v) return factors # Otherwise, just return intervals as floats ratios: np.ndarray = np.power(2, log_ratios) return ratios ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2099245 librosa-1.0.0/librosa/core/notation.py0000644000175100017510000010661515236640653017424 0ustar00runnerrunner#!/usr/bin/env python # -*- coding: utf-8 -*- """Music notation utilities""" from __future__ import annotations import re from collections import Counter from typing import TYPE_CHECKING, overload import numpy as np from numba import jit from .._cache import cache from ..util.decorators import vectorize from ..util.exceptions import ParameterError from .intervals import INTERVALS if TYPE_CHECKING: from typing import Iterable import numpy.typing as npt from .._typing import ( _Array1D, _FloatLike_co, _IterableLike, _ScalarOrSequence, _SequenceLike, ) __all__ = [ "key_to_degrees", "key_to_notes", "mela_to_degrees", "mela_to_svara", "thaat_to_degrees", "list_mela", "list_thaat", "fifths_to_note", "interval_to_fjs", ] THAAT_MAP = dict( bilaval=[0, 2, 4, 5, 7, 9, 11], khamaj=[0, 2, 4, 5, 7, 9, 10], kafi=[0, 2, 3, 5, 7, 9, 10], asavari=[0, 2, 3, 5, 7, 8, 10], bhairavi=[0, 1, 3, 5, 7, 8, 10], kalyan=[0, 2, 4, 6, 7, 9, 11], marva=[0, 1, 4, 6, 7, 9, 11], poorvi=[0, 1, 4, 6, 7, 8, 11], todi=[0, 1, 3, 6, 7, 8, 11], bhairav=[0, 1, 4, 5, 7, 8, 11], ) # Enumeration will start from 1 MELAKARTA_MAP = { k: i for i, k in enumerate( [ "kanakangi", "ratnangi", "ganamurthi", "vanaspathi", "manavathi", "tanarupi", "senavathi", "hanumathodi", "dhenuka", "natakapriya", "kokilapriya", "rupavathi", "gayakapriya", "vakulabharanam", "mayamalavagaula", "chakravakom", "suryakantham", "hatakambari", "jhankaradhwani", "natabhairavi", "keeravani", "kharaharapriya", "gaurimanohari", "varunapriya", "mararanjini", "charukesi", "sarasangi", "harikambhoji", "dheerasankarabharanam", "naganandini", "yagapriya", "ragavardhini", "gangeyabhushani", "vagadheeswari", "sulini", "chalanatta", "salagam", "jalarnavam", "jhalavarali", "navaneetham", "pavani", "raghupriya", "gavambodhi", "bhavapriya", "subhapanthuvarali", "shadvidhamargini", "suvarnangi", "divyamani", "dhavalambari", "namanarayani", "kamavardhini", "ramapriya", "gamanasrama", "viswambhari", "syamalangi", "shanmukhapriya", "simhendramadhyamam", "hemavathi", "dharmavathi", "neethimathi", "kanthamani", "rishabhapriya", "latangi", "vachaspathi", "mechakalyani", "chitrambari", "sucharitra", "jyotisvarupini", "dhatuvardhini", "nasikabhushani", "kosalam", "rasikapriya", ], 1, ) } # Pre-compiled regular expressions for note and key parsing KEY_RE = re.compile( r"^(?P[A-Ga-g])" r"(?P[#♯ð�„ªb!â™­ð�„«â™®n]*)" r":((?P(maj|min)(or)?)|(?P(((ion|dor|phryg|lyd|mixolyd|aeol|locr)(ian)?)|phr|mix|aeo|loc)))$" ) NOTE_RE = re.compile( r"^(?P[A-Ga-g])" r"(?P[#♯ð�„ªb!â™­ð�„«â™®n]*)" r"(?P[+-]?\d+)?" r"(?P[+-]\d+)?$" ) # A dictionary converting the tonic name to the associated major key, # e.g. C Dorian uses the notes of the Bb major scale, hence MAJOR_DICT['dor']['C'] = 'Bâ™­' MAJOR_DICT = { "ion": {"C": "C", "D": "D", "E": "E", "F": "F", "G": "G", "A": "A", "B": "B"}, "dor": {"C": "Bâ™­", "D": "C", "E": "D", "F": "Eâ™­", "G": "F", "A": "G", "B": "A"}, "phr": {"C": "Aâ™­", "D": "Bâ™­", "E": "C", "F": "Dâ™­", "G": "Eâ™­", "A": "F", "B": "G"}, "lyd": {"C": "G", "D": "A", "E": "B", "F": "C", "G": "D", "A": "E", "B": "F♯"}, "mix": {"C": "F", "D": "G", "E": "A", "F": "Bâ™­", "G": "C", "A": "D", "B": "E"}, "aeo": {"C": "Eâ™­", "D": "F", "E": "G", "F": "Aâ™­", "G": "Bâ™­", "A": "C", "B": "D"}, "loc": {"C": "Dâ™­", "D": "Eâ™­", "E": "F", "F": "Gâ™­", "G": "Aâ™­", "A": "Bâ™­", "B": "C"} } OFFSET_DICT = { "ion": 0, "dor": 1, "phr": 2, "lyd": 3, "mix": 4, "aeo": 5, "loc": 6 } ACC_MAP = {"#": 1, "â™®": 0, "": 0, "n": 0, "b": -1, "!": -1, "♯": 1, "â™­": -1, "ð�„ª": 2, "ð�„«": -2} def thaat_to_degrees(thaat: str) -> _Array1D[np.int_]: """Construct the svara indices (degrees) for a given thaat Parameters ---------- thaat : str The name of the thaat Returns ------- indices : np.ndarray A list of the seven svara indices (starting from 0=Sa) contained in the specified thaat See Also -------- key_to_degrees mela_to_degrees list_thaat Examples -------- >>> librosa.thaat_to_degrees('bilaval') array([ 0, 2, 4, 5, 7, 9, 11]) >>> librosa.thaat_to_degrees('todi') array([ 0, 1, 3, 6, 7, 8, 11]) """ return np.asarray(THAAT_MAP[thaat.lower()]) def mela_to_degrees(mela: str | int) -> _Array1D[np.int_]: """Construct the svara indices (degrees) for a given melakarta raga Parameters ---------- mela : str or int Either the name or integer index ([1, 2, ..., 72]) of the melakarta raga Returns ------- degrees : np.ndarray A list of the seven svara indices (starting from 0=Sa) contained in the specified raga See Also -------- thaat_to_degrees key_to_degrees list_mela Examples -------- Melakarta #1 (kanakangi): >>> librosa.mela_to_degrees(1) array([0, 1, 2, 5, 7, 8, 9]) Or using a name directly: >>> librosa.mela_to_degrees('kanakangi') array([0, 1, 2, 5, 7, 8, 9]) """ if isinstance(mela, str): index = MELAKARTA_MAP[mela.lower()] - 1 elif 0 < mela <= 72: index = mela - 1 else: raise ParameterError(f"mela={mela} must be in range [1, 72]") # always have Sa [0] degrees = [0] # Fill in Ri and Ga lower = index % 36 if 0 <= lower < 6: # Ri1, Ga1 degrees.extend([1, 2]) elif 6 <= lower < 12: # Ri1, Ga2 degrees.extend([1, 3]) elif 12 <= lower < 18: # Ri1, Ga3 degrees.extend([1, 4]) elif 18 <= lower < 24: # Ri2, Ga2 degrees.extend([2, 3]) elif 24 <= lower < 30: # Ri2, Ga3 degrees.extend([2, 4]) else: # Ri3, Ga3 degrees.extend([3, 4]) # Determine Ma if index < 36: # Ma1 degrees.append(5) else: # Ma2 degrees.append(6) # always have Pa [7] degrees.append(7) # Determine Dha and Ni upper = index % 6 if upper == 0: # Dha1, Ni1 degrees.extend([8, 9]) elif upper == 1: # Dha1, Ni2 degrees.extend([8, 10]) elif upper == 2: # Dha1, Ni3 degrees.extend([8, 11]) elif upper == 3: # Dha2, Ni2 degrees.extend([9, 10]) elif upper == 4: # Dha2, Ni3 degrees.extend([9, 11]) else: # Dha3, Ni3 degrees.extend([10, 11]) return np.array(degrees) @cache(level=10) def mela_to_svara( mela: str | int, *, abbr: bool = True, unicode: bool = True ) -> list[str]: """Spell the Carnatic svara names for a given melakarta raga This function exists to resolve enharmonic equivalences between pitch classes: - Ri2 / Ga1 - Ri3 / Ga2 - Dha2 / Ni1 - Dha3 / Ni2 For svara outside the raga, names are chosen to preserve orderings so that all Ri precede all Ga, and all Dha precede all Ni. Parameters ---------- mela : str or int the name or numerical index of the melakarta raga abbr : bool If `True`, use single-letter svara names: S, R, G, ... If `False`, use full names: Sa, Ri, Ga, ... unicode : bool If `True`, use unicode symbols for numberings, e.g., Ri\u2081 If `False`, use low-order ASCII, e.g., Ri1. Returns ------- svara : list of str The svara names for each of the 12 pitch classes. See Also -------- key_to_notes mela_to_degrees list_mela Examples -------- Melakarta #1 (Kanakangi) uses R1, G1, D1, N1 >>> librosa.mela_to_svara(1) ['S', 'Râ‚�', 'Gâ‚�', 'Gâ‚‚', 'G₃', 'Mâ‚�', 'Mâ‚‚', 'P', 'Dâ‚�', 'Nâ‚�', 'Nâ‚‚', 'N₃'] #19 (Jhankaradhwani) uses R2 and G2 so the third svara are Ri: >>> librosa.mela_to_svara(19) ['S', 'Râ‚�', 'Râ‚‚', 'Gâ‚‚', 'G₃', 'Mâ‚�', 'Mâ‚‚', 'P', 'Dâ‚�', 'Nâ‚�', 'Nâ‚‚', 'N₃'] #31 (Yagapriya) uses R3 and G3, so third and fourth svara are Ri: >>> librosa.mela_to_svara(31) ['S', 'Râ‚�', 'Râ‚‚', 'R₃', 'G₃', 'Mâ‚�', 'Mâ‚‚', 'P', 'Dâ‚�', 'Nâ‚�', 'Nâ‚‚', 'N₃'] #34 (Vagadheeswari) uses D2 and N2, so Ni1 becomes Dha2: >>> librosa.mela_to_svara(34) ['S', 'Râ‚�', 'Râ‚‚', 'R₃', 'G₃', 'Mâ‚�', 'Mâ‚‚', 'P', 'Dâ‚�', 'Dâ‚‚', 'Nâ‚‚', 'N₃'] #36 (Chalanatta) uses D3 and N3, so Ni2 becomes Dha3: >>> librosa.mela_to_svara(36) ['S', 'Râ‚�', 'Râ‚‚', 'R₃', 'G₃', 'Mâ‚�', 'Mâ‚‚', 'P', 'Dâ‚�', 'Dâ‚‚', 'D₃', 'N₃'] # You can also query by raga name instead of index: >>> librosa.mela_to_svara('chalanatta') ['S', 'Râ‚�', 'Râ‚‚', 'R₃', 'G₃', 'Mâ‚�', 'Mâ‚‚', 'P', 'Dâ‚�', 'Dâ‚‚', 'D₃', 'N₃'] """ # The following will be constant for all ragas svara_map = [ "Sa", "Ri\u2081", "", # Ri2/Ga1 "", # Ri3/Ga2 "Ga\u2083", "Ma\u2081", "Ma\u2082", "Pa", "Dha\u2081", "", # Dha2/Ni1 "", # Dha3/Ni2 "Ni\u2083", ] if isinstance(mela, str): mela_idx = MELAKARTA_MAP[mela.lower()] - 1 elif 0 < mela <= 72: mela_idx = mela - 1 else: raise ParameterError(f"mela={mela} must be in range [1, 72]") # Determine Ri2/Ga1 lower = mela_idx % 36 if lower < 6: # First six will have Ri1/Ga1 svara_map[2] = "Ga\u2081" else: # All others have either Ga2/Ga3 # So we'll call this Ri2 svara_map[2] = "Ri\u2082" # Determine Ri3/Ga2 if lower < 30: # First thirty should get Ga2 svara_map[3] = "Ga\u2082" else: # Only the last six have Ri3 svara_map[3] = "Ri\u2083" upper = mela_idx % 6 # Determine Dha2/Ni1 if upper == 0: # these are the only ones with Ni1 svara_map[9] = "Ni\u2081" else: # Everyone else has Dha2 svara_map[9] = "Dha\u2082" # Determine Dha3/Ni2 if upper == 5: # This one has Dha3 svara_map[10] = "Dha\u2083" else: # Everyone else has Ni2 svara_map[10] = "Ni\u2082" if abbr: t_abbr = str.maketrans({"a": "", "h": "", "i": ""}) svara_map = [s.translate(t_abbr) for s in svara_map] if not unicode: t_uni = str.maketrans({"\u2081": "1", "\u2082": "2", "\u2083": "3"}) svara_map = [s.translate(t_uni) for s in svara_map] return list(svara_map) def list_mela() -> dict[str, int]: """List melakarta ragas by name and index. Melakarta raga names are transcribed from [#]_, with the exception of #45 (subhapanthuvarali). .. [#] Bhagyalekshmy, S. (1990). Ragas in Carnatic music. South Asia Books. Returns ------- mela_map : dict A dictionary mapping melakarta raga names to indices (1, 2, ..., 72) Examples -------- >>> librosa.list_mela() {'kanakangi': 1, 'ratnangi': 2, 'ganamurthi': 3, 'vanaspathi': 4, ...} See Also -------- mela_to_degrees mela_to_svara list_thaat """ return MELAKARTA_MAP.copy() def list_thaat() -> list[str]: """List supported thaats by name. Returns ------- thaats : list A list of supported thaats Examples -------- >>> librosa.list_thaat() ['bilaval', 'khamaj', 'kafi', 'asavari', 'bhairavi', 'kalyan', 'marva', 'poorvi', 'todi', 'bhairav'] See Also -------- list_mela thaat_to_degrees """ return list(THAAT_MAP.keys()) @overload def __note_to_degree(key: str) -> int: ... @overload def __note_to_degree(key: _IterableLike[str]) -> _Array1D[np.int_]: ... @overload def __note_to_degree(key: Iterable[str]) -> int | _Array1D[np.int_]: ... def __note_to_degree(key: str | _IterableLike[str] | Iterable[str]) -> int | _Array1D[np.int_]: """Take a note name and return the degree of that note (e.g. 'C#' -> 1). We allow possibilities like "C#b". >>> librosa.__note_to_degree('B#') 0 >>> librosa.__note_to_degree('Dâ™®##b') 3 >>> librosa.__note_to_degree(['B#','Dâ™®##b']) array([0,3]) """ if not isinstance(key, str): return np.array([__note_to_degree(n) for n in key]) match = NOTE_RE.match(key) if not match: raise ParameterError(f"Improper key format: {key:s}") letter = match.group("note").upper() accidental = match.group("accidental") pitch_map = {"C": 0, "D": 2, "E": 4, "F": 5, "G": 7, "A": 9, "B": 11} counter = Counter(accidental) return (pitch_map[letter]+sum([ACC_MAP[acc] * counter[acc] for acc in ACC_MAP]))%12 @overload def __simplify_note(key: str, additional_acc: str =..., unicode: bool = ...) -> str: ... @overload def __simplify_note( key: _IterableLike[str], additional_acc: str = ..., unicode: bool = ... ) -> npt.NDArray[np.str_]: ... @overload def __simplify_note( key: Iterable[str], additional_acc: str = ..., unicode: bool = ... ) -> str | npt.NDArray[np.str_]: ... def __simplify_note( key: str | _IterableLike[str] | Iterable[str], additional_acc: str = "", unicode: bool = True ) -> str | npt.NDArray[np.str_]: """Take in a note name and simplify by canceling sharp-flat pairs, and doubling accidentals as appropriate. >>> librosa.__simplify_note('C♭♯') 'C' >>> librosa.__simplify_note('Câ™­â™­â™­') 'Câ™­ð�„«' >>> librosa.__simplify_note(['C♭♯', 'Câ™­â™­â™­']) array(['C', 'Câ™­ð�„«'], dtype='=0: simplified_note += "♯"*(offset%2)+ "ð�„ª"*(offset//2) else: simplified_note += "â™­"*(offset%2)+ "ð�„«"*(abs(offset)//2) if not unicode: translations = str.maketrans({"♯": "#", "ð�„ª": "##", "â™­": "b", "ð�„«": "bb", "â™®": "n"}) simplified_note = simplified_note.translate(translations) return simplified_note def __mode_to_key(signature: str, unicode: bool = True) -> str: """Translate a mode (eg D:dorian) into its equivalent major key. If unicode==True, return the accidentals as unicode symbols, regardless of nature of accidentals in signature. Otherwise, return accidentals as ASCII symbols. >>> librosa.__mode_to_key('Db:loc') 'Eð�„«:maj' >>> librosa.__mode_to_key('Dâ™­:loc', unicode = False) 'Ebb:maj' """ match = KEY_RE.match(signature) if not match: raise ParameterError("Improper format: {:s}".format(signature)) if match.group("scale") or not match.group("mode"): # We're already fine here, but let's pass the key through __simpify_note() # to ensure good formatting. signature = (__simplify_note(match.group("tonic").upper()+match.group("accidental"), unicode=unicode) + (":"+match.group("scale") if match.group("scale") else "")) return signature # We have a mode, time to translate mode = match.group("mode").lower()[:3] # Get the relative major tonic = MAJOR_DICT[mode][match.group("tonic").upper()] return __simplify_note(tonic+match.group("accidental"), unicode = unicode)+":maj" @cache(level=10) def key_to_notes(key: str, *, unicode: bool = True, natural: bool= False) -> list[str]: """List all 12 chromatic note names, as spelled according to a given key or mode. This function exists to resolve enharmonic equivalences between different spellings for the same pitch (e.g. C♯ vs Dâ™­), and is primarily useful when producing human-readable outputs (e.g. plotting) for pitch content. Note names are decided by the following rules: 1. If the tonic of the key has an accidental (sharp or flat), that accidental will be used consistently for all notes. 2. If the tonic does not have an accidental, accidentals will be inferred to minimize the total number used for diatonic scale degrees. 3. If there is a tie (e.g., in the case of C:maj vs A:min), sharps will be preferred. Parameters ---------- key : str Must be in the form TONIC:key. Tonic must be upper case (``CDEFGAB``), key must be lower-case (``major``, ``minor``, ``ionian``, ``dorian``, ``phrygian``, ``lydian``, ``mixolydian``, ``aeolian``, ``locrian``). The following abbreviations are supported for the modes: either the first three letters of the mode name (e.g. "mix") or the mode name without "ian" (e.g. "mixolyd"). Both ``major`` and ``maj`` are supported as mode abbreviations. Single and multiple accidentals (``b!â™­`` for flat, ``#♯`` for sharp, ``ð�„ªð�„«`` for double-accidentals, or any combination thereof) are supported. Examples: ``C:maj, C:major, Dbb:min, Aâ™­:min, D:aeo, Eð�„ª:phryg``. unicode : bool If ``True`` (default), use Unicode symbols (♯ð�„ªâ™­ð�„«)for accidentals. If ``False`` Unicode symbols will be mapped to low-order ASCII representations:: ♯ -> #, ð�„ª -> ##, â™­ -> b, ð�„« -> bb, â™® -> n natural : bool If ``True'' mark natural accidentals with a natural symbol (â™®). If ``False`` (default), do not print natural symbols. For example, `note_to_degrees('D:maj')[0]` is `C` if `natural=False` (default) and `Câ™®` if `natural=True`. Returns ------- notes : list ``notes[k]`` is the name for semitone ``k`` (starting from C) under the given key. All chromatic notes (0 through 11) are included. See Also -------- midi_to_note Examples -------- `C:maj` will use all sharps >>> librosa.key_to_notes('C:maj') ['C', 'C♯', 'D', 'D♯', 'E', 'F', 'F♯', 'G', 'G♯', 'A', 'A♯', 'B'] `A:min` has the same notes >>> librosa.key_to_notes('A:min') ['C', 'C♯', 'D', 'D♯', 'E', 'F', 'F♯', 'G', 'G♯', 'A', 'A♯', 'B'] `A♯:min` will use sharps, but spell note 0 (`C`) as `B♯` >>> librosa.key_to_notes('A#:min') ['B♯', 'C♯', 'D', 'D♯', 'E', 'E♯', 'F♯', 'G', 'G♯', 'A', 'A♯', 'B'] `G♯:maj` will use a double-sharp to spell note 7 (`G`) as `Fð�„ª`: >>> librosa.key_to_notes('G#:maj') ['B♯', 'C♯', 'D', 'D♯', 'E', 'E♯', 'F♯', 'Fð�„ª', 'G♯', 'A', 'A♯', 'B'] `Fâ™­:min` will use double-flats >>> librosa.key_to_notes('Fb:min') ['Dð�„«', 'Dâ™­', 'Eð�„«', 'Eâ™­', 'Fâ™­', 'F', 'Gâ™­', 'Að�„«', 'Aâ™­', 'Bð�„«', 'Bâ™­', 'Câ™­'] `G:loc` uses flats >>> librosa.key_to_notes('G:loc') ['C', 'Dâ™­', 'D', 'Eâ™­', 'E', 'F', 'Gâ™­', 'G', 'Aâ™­', 'A', 'Bâ™­', 'B'] If `natural=True`, print natural accidentals. >>> librosa.key_to_notes('G:loc', natural=True) ['C', 'Dâ™­', 'Dâ™®', 'Eâ™­', 'Eâ™®', 'F', 'Gâ™­', 'G', 'Aâ™­', 'Aâ™®', 'Bâ™­', 'Bâ™®'] >>> librosa.key_to_notes('D:maj', natural=True) ['Câ™®', 'C♯', 'D', 'D♯', 'E', 'Fâ™®', 'F♯', 'G', 'G♯', 'A', 'A♯', 'B'] >>> librosa.key_to_notes('G#:maj', unicode = False, natural = True) ['B#', 'C#', 'Dn', 'D#', 'En', 'E#', 'F#', 'F##', 'G#', 'An', 'A#', 'B'] We can combine this with ``key_to_degrees`` to get the notes for a given scale: >>> notes = librosa.key_to_notes('D:maj') >>> degrees = librosa.key_to_degrees('D:maj') >>> print([notes[d] for d in degrees]) ['D', 'E', 'F♯', 'G', 'A', 'B', 'C♯'] """ # Parse the key signature match = KEY_RE.match(key) if not match: raise ParameterError(f"Improper key format: {key:s}") pitch_map = {"C": 0, "D": 2, "E": 4, "F": 5, "G": 7, "A": 9, "B": 11} tonic = match.group("tonic").upper() accidental = match.group("accidental") offset = sum([ACC_MAP[acc] for acc in accidental]) if match.group("mode") or not match.group("scale"): equiv = __mode_to_key(key) return key_to_notes(equiv, unicode=unicode, natural = natural) scale = match.group("scale")[:3].lower() multiple = abs(offset)>=2 # If multiple accidentals, we use recursion, then cycle through so that the # enharmonic equivalent of C is at the beginning again. if multiple: sign_map = {+1: "♯", -1: "â™­"} additional_acc = sign_map[np.sign(offset)] intermediate_notes = key_to_notes(tonic+additional_acc*(abs(offset)-1)+":"+scale, natural = False) notes = [__simplify_note(note, additional_acc) for note in intermediate_notes] degrees = __note_to_degree(notes) notes = np.roll(notes, shift=-np.argwhere(degrees == 0)[0]) notes = list(notes) if not unicode: translations = str.maketrans({"♯": "#", "ð�„ª": "##", "â™­": "b", "ð�„«": "bb", "â™®": "n"}) notes = list(n.translate(translations) for n in notes) return notes # Determine major or minor major = scale == "maj" # calculate how many clockwise steps we are on CoF (== # sharps) if major: tonic_number = ((pitch_map[tonic] + offset) * 7) % 12 else: tonic_number = ((pitch_map[tonic] + offset) * 7 + 9) % 12 # Decide if using flats or sharps # Logic here is as follows: # 1. respect the given notation for the tonic. # Sharp tonics will always use sharps, likewise flats. # 2. If no accidental in the tonic, try to minimize accidentals. # 3. If there's a tie for accidentals, use sharp for major and flat for minor. if offset < 0: # use flats explicitly use_sharps = False elif offset > 0: # use sharps explicitly use_sharps = True elif 0 <= tonic_number < 6: use_sharps = True elif tonic_number > 6: use_sharps = False # Basic note sequences for simple keys notes_sharp = ["C", "C♯", "D", "D♯", "E", "F", "F♯", "G", "G♯", "A", "A♯", "B"] notes_flat = ["C", "Dâ™­", "D", "Eâ™­", "E", "F", "Gâ™­", "G", "Aâ™­", "A", "Bâ™­", "B"] # These apply when we have >= 6 sharps sharp_corrections = [ (5, "E♯"), (0, "B♯"), (7, "Fð�„ª"), (2, "Cð�„ª"), (9, "Gð�„ª"), (4, "Dð�„ª"), (11, "Að�„ª"), ] # These apply when we have >= 6 flats flat_corrections = [ (11, "Câ™­"), (4, "Fâ™­"), (9, "Bð�„«"), (2, "Eð�„«"), (7, "Að�„«"), (0, "Dð�„«"), ] # last would be (5, 'Gð�„«') # Apply a mod-12 correction to distinguish B#:maj from C:maj n_sharps = tonic_number if tonic_number == 0 and tonic == "B": n_sharps = 12 if use_sharps: # This will only execute if n_sharps >= 6 for n in range(0, n_sharps - 6 + 1): index, name = sharp_corrections[n] notes_sharp[index] = name notes = notes_sharp else: n_flats = (12 - tonic_number) % 12 # This will only execute if tonic_number <= 6 for n in range(0, n_flats - 6 + 1): index, name = flat_corrections[n] notes_flat[index] = name notes = notes_flat # Apply natural signs to any note which has no other accidentals and does not appear in the scale for key. if natural: scale_notes = set(key_to_degrees(key)) for place, note in enumerate(notes): if __note_to_degree(note) in scale_notes: continue if len(note)==1: notes[place] = note+"â™®" # Finally, apply any unicode down-translation if necessary if not unicode: translations = str.maketrans({"♯": "#", "ð�„ª": "##", "â™­": "b", "ð�„«": "bb", "â™®": "n"}) notes = list(n.translate(translations) for n in notes) return notes # I made this work even for key signatures like 'C#b#:min' def key_to_degrees(key: str) -> np.ndarray: """Construct the diatonic scale degrees for a given key. Parameters ---------- key : str Must be in the form TONIC:key. Tonic must be upper case (``CDEFGAB``), key must be lower-case (``maj``, ``min``, ``ionian``, ``dorian``, ``phrygian``, ``lydian``, ``mixolydian``, ``aeolian``, ``locrian``). The following abbreviations are supported for the modes: either the first three letters of the mode name (e.g. "mix") or the mode name without "ian" (e.g. "mixolyd"). Both ``major`` and ``maj`` are supported as abbreviations. Single and multiple accidentals (``b!â™­`` for flat, or ``#♯`` for sharp) are supported. Examples: ``C:maj, C:major, Dbb:min, Aâ™­:min, D:aeo, Eð�„ª:phryg``. Returns ------- degrees : np.ndarray An array containing the semitone numbers (0=C, 1=C#, ... 11=B) for each of the seven scale degrees in the given key, starting from the tonic. See Also -------- key_to_notes Examples -------- >>> librosa.key_to_degrees('C:maj') array([ 0, 2, 4, 5, 7, 9, 11]) >>> librosa.key_to_degrees('C#:maj') array([ 1, 3, 5, 6, 8, 10, 0]) >>> librosa.key_to_degrees('A:min') array([ 9, 11, 0, 2, 4, 5, 7]) >>> librosa.key_to_degrees('A:min') array([ 9, 11, 0, 2, 4, 5, 7]) """ notes = dict( maj=np.array([0, 2, 4, 5, 7, 9, 11]), min=np.array([0, 2, 3, 5, 7, 8, 10]) ) match = KEY_RE.match(key) if not match: raise ParameterError(f"Improper key format: {key:s}") if match.group("mode") or not match.group("scale"): equiv = __mode_to_key(key) offset = OFFSET_DICT[match.group("mode")[:3]] return np.roll(key_to_degrees(equiv),-offset) pitch_map = {"C": 0, "D": 2, "E": 4, "F": 5, "G": 7, "A": 9, "B": 11} tonic = match.group("tonic").upper() accidental = match.group("accidental") counts = Counter(accidental) offset = sum([ACC_MAP[acc]*counts[acc] for acc in ACC_MAP]) scale = match.group("scale")[:3].lower() return (notes[scale] + pitch_map[tonic] + offset) % 12 @cache(level=10) def fifths_to_note(*, unison: str, fifths: int, unicode: bool = True) -> str: """Calculate the note name for a given number of perfect fifths from a specified unison. This function is primarily intended as a utility routine for Functional Just System (FJS) notation conversions. This function does not assume the "circle of fifths" or equal temperament, so 12 fifths will not generally produce a note of the same pitch class due to the accumulation of accidentals. Parameters ---------- unison : str The name of the starting (unison) note, e.g., 'C' or 'Bb'. Unicode accidentals are supported. fifths : int The number of perfect fifths to deviate from unison. unicode : bool If ``True`` (default), use Unicode symbols (♯ð�„ªâ™­ð�„«)for accidentals. If ``False``, accidentals will be encoded as low-order ASCII representations:: ♯ -> #, ð�„ª -> ##, â™­ -> b, ð�„« -> bb Returns ------- note : str The name of the requested note Examples -------- >>> librosa.fifths_to_note(unison='C', fifths=6) 'F♯' >>> librosa.fifths_to_note(unison='G', fifths=-3) 'Bâ™­' >>> librosa.fifths_to_note(unison='Eb', fifths=11, unicode=False) 'G#' """ # Starting the circle of fifths at F makes accidentals easier to count COFMAP = "FCGDAEB" acc_map = { "#": 1, "": 0, "b": -1, "!": -1, "♯": 1, "ð�„ª": 2, "â™­": -1, "ð�„«": -2, "â™®": 0, "n": 0 } if unicode: acc_map_inv = {1: "♯", 2: "ð�„ª", -1: "â™­", -2: "ð�„«", 0: ""} else: acc_map_inv = {1: "#", 2: "##", -1: "b", -2: "bb", 0: ""} match = NOTE_RE.match(unison) if not match: raise ParameterError(f"Improper note format: {unison:s}") # Find unison in the alphabet pitch = match.group("note").upper() # Find the number of accidentals to start from offset = np.sum([acc_map[o] for o in match.group("accidental")]) # Find the raw target note circle_idx = COFMAP.index(pitch) raw_output = COFMAP[(circle_idx + fifths) % 7] # Now how many accidentals have we accrued? # Equivalently, count times we cross a B<->F boundary acc_index = offset + (circle_idx + fifths) // 7 # Compress multiple-accidentals as needed acc_str = acc_map_inv[np.sign(acc_index) * 2] * int( abs(acc_index) // 2 ) + acc_map_inv[np.sign(acc_index)] * int(abs(acc_index) % 2) return raw_output + acc_str @jit(nopython=True, nogil=True, cache=True) def __o_fold(d): """Compute the octave-folded interval. This maps intervals to the range [1, 2). This is part of the FJS notation converter. It is equivalent to the `red` function described in the FJS documentation. """ return d * (2.0 ** -np.floor(np.log2(d))) @jit(nopython=True, nogil=True, cache=True) def __bo_fold(d): """Compute the balanced, octave-folded interval. This maps intervals to the range [sqrt(2)/2, sqrt(2)). This is part of the FJS notation converter. It is equivalent to the `reb` function described in the FJS documentation, but with a simpler implementation. """ return d * (2.0 ** -np.round(np.log2(d))) @jit(nopython=True, nogil=True, cache=True) def __fifth_search(interval, tolerance): """Accelerated helper function for finding the number of fifths to get within tolerance of a given interval. This implementation will give up after 32 fifths """ log_tolerance = np.abs(np.log2(tolerance)) for power in range(32): for sign in [1, -1]: if ( np.abs(np.log2(__bo_fold(interval / 3.0 ** (power * sign)))) <= log_tolerance ): return power * sign power += 1 return power # Translation grids for superscripts and subscripts SUPER_TRANS = str.maketrans("0123456789", "â�°Â¹Â²Â³â�´â�µâ�¶â�·â�¸â�¹") SUB_TRANS = str.maketrans("0123456789", "â‚€â‚�₂₃₄₅₆₇₈₉") @overload def interval_to_fjs( interval: _FloatLike_co, *, unison: str = ..., tolerance: float = ..., unicode: bool = ..., ) -> str: ... @overload def interval_to_fjs( interval: _SequenceLike[_FloatLike_co], *, unison: str = ..., tolerance: float = ..., unicode: bool = ..., ) -> npt.NDArray[np.str_]: ... @vectorize(otypes="U", excluded=set(["unison", "tolerance", "unicode"])) def interval_to_fjs( interval: _ScalarOrSequence[_FloatLike_co], *, unison: str = "C", tolerance: float = 65.0 / 63, unicode: bool = True, ) -> str | npt.NDArray[np.str_]: """Convert an interval to Functional Just System (FJS) notation. See https://misotanni.github.io/fjs/en/index.html for a thorough overview of the FJS notation system, and the examples below. FJS conversion works by identifying a Pythagorean interval which is within a specified tolerance of the target interval, which provides the core note name. If the interval is derived from ratios other than perfect fifths, then the remaining factors are encoded as superscripts for otonal (increasing) intervals and subscripts for utonal (decreasing) intervals. Parameters ---------- interval : float > 0 or iterable of floats A (just) interval to notate in FJS. unison : str The name of the unison note (corresponding to `interval=1`). tolerance : float The tolerance threshold for identifying the core note name. unicode : bool If ``True`` (default), use Unicode symbols (♯ð�„ªâ™­ð�„«)for accidentals, and superscripts/subscripts for otonal and utonal accidentals. If ``False``, accidentals will be encoded as low-order ASCII representations:: ♯ -> #, ð�„ª -> ##, â™­ -> b, ð�„« -> bb Otonal and utonal accidentals will be denoted by `^##` and `_##` respectively (see examples below). Raises ------ ParameterError If the provided interval is not positive If the provided interval cannot be identified with a just intonation prime factorization. Returns ------- note_fjs : str or np.ndarray(dtype=str) The interval(s) relative to the given unison in FJS notation. Examples -------- Pythagorean intervals appear as expected, with no otonal or utonal extensions: >>> librosa.interval_to_fjs(3/2, unison='C') 'G' >>> librosa.interval_to_fjs(4/3, unison='F') 'Bâ™­' A ptolemaic major third will appear with an otonal '5': >>> librosa.interval_to_fjs(5/4, unison='A') 'C♯â�µ' And a ptolemaic minor third will appear with utonal '5': >>> librosa.interval_to_fjs(6/5, unison='A') 'Câ‚…' More complex intervals will have compound accidentals. For example: >>> librosa.interval_to_fjs(25/14, unison='F#') 'E²â�µâ‚‡' >>> librosa.interval_to_fjs(25/14, unison='F#', unicode=False) 'E^25_7' Array inputs are also supported: >>> librosa.interval_to_fjs([3/2, 4/3, 5/3]) array(['G', 'F', 'Aâ�µ'], dtype=' 3} # Split into otonal and utonal accidentals otonal = np.prod([p ** powers[p] for p in powers if powers[p] > 0]) utonal = np.prod([p ** -powers[p] for p in powers if powers[p] < 0]) suffix = "" if otonal > 1: if unicode: suffix += f"{otonal:d}".translate(SUPER_TRANS) else: suffix += f"^{otonal}" if utonal > 1: if unicode: suffix += f"{utonal:d}".translate(SUB_TRANS) else: suffix += f"_{utonal}" return note_name + suffix ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2102268 librosa-1.0.0/librosa/core/pitch.py0000644000175100017510000010227315236640653016674 0ustar00runnerrunner#!/usr/bin/env python # -*- coding: utf-8 -*- """Pitch-tracking and tuning estimation""" from __future__ import annotations import warnings from typing import TYPE_CHECKING import numba import numpy as np from .. import sequence, util from .._cache import cache from ..util.exceptions import ParameterError from . import audio, convert from .spectrum import _spectrogram if TYPE_CHECKING: from typing import Any, Callable from numpy.typing import ArrayLike from .._typing import _PadMode, _PadModeSTFT, _WindowSpec __all__ = ["estimate_tuning", "pitch_tuning", "piptrack", "yin", "pyin"] def estimate_tuning( *, y: np.ndarray | None = None, sr: float = 22050, S: np.ndarray | None = None, n_fft: int | None = 2048, resolution: float = 0.01, bins_per_octave: int = 12, **kwargs: Any, ) -> float: """Estimate the tuning of an audio time series or spectrogram input. Parameters ---------- y : np.ndarray [shape=(..., n)] or None audio signal. Multi-channel is supported. sr : number > 0 [scalar] audio sampling rate of ``y`` S : np.ndarray [shape=(..., d, t)] or None magnitude or power spectrogram n_fft : int > 0 [scalar] or None length of the FFT frame to use, if ``y`` is provided. resolution : float in `(0, 1)` Resolution of the tuning as a fraction of a bin. 0.01 corresponds to measurements in cents. bins_per_octave : int > 0 [scalar] How many frequency bins per octave **kwargs : additional keyword arguments Additional arguments passed to `piptrack` Returns ------- tuning: float in `[-0.5, 0.5)` estimated tuning deviation (fractions of a bin). Note that if multichannel input is provided, a single tuning estimate is provided spanning all channels. See Also -------- piptrack : Pitch tracking by parabolic interpolation Examples -------- With time-series input >>> y, sr = librosa.loadx('trumpet') >>> librosa.estimate_tuning(y=y, sr=sr) -0.08000000000000002 In tenths of a cent >>> librosa.estimate_tuning(y=y, sr=sr, resolution=1e-3) -0.016000000000000014 Using spectrogram input >>> S = np.abs(librosa.stft(y)) >>> librosa.estimate_tuning(S=S, sr=sr) -0.08000000000000002 Using pass-through arguments to `librosa.piptrack` >>> librosa.estimate_tuning(y=y, sr=sr, n_fft=8192, ... fmax=librosa.note_to_hz('G#9')) -0.08000000000000002 """ pitch, mag = piptrack(y=y, sr=sr, S=S, n_fft=n_fft, **kwargs) # Only count magnitude where frequency is > 0 pitch_mask = pitch > 0 if pitch_mask.any(): threshold = np.median(mag[pitch_mask]) else: threshold = 0.0 return pitch_tuning( pitch[(mag >= threshold) & pitch_mask], resolution=resolution, bins_per_octave=bins_per_octave, ) def pitch_tuning( frequencies: ArrayLike, *, resolution: float = 0.01, bins_per_octave: int = 12 ) -> float: """Given a collection of pitches, estimate its tuning offset relative to A440=440.0Hz. Parameters ---------- frequencies : array-like, float A collection of frequencies detected in the signal. See `piptrack` resolution : float in `(0, 1)` Resolution of the tuning as a fraction of a bin. 0.01 corresponds to cents. bins_per_octave : int > 0 [scalar] How many frequency bins per octave Returns ------- tuning: float in `[-0.5, 0.5)` estimated tuning deviation (fractions of a bin) See Also -------- estimate_tuning : Estimating tuning from time-series or spectrogram input Examples -------- >>> # Generate notes at +25 cents >>> freqs = librosa.cqt_frequencies(n_bins=24, fmin=55, tuning=0.25) >>> librosa.pitch_tuning(freqs) 0.25 >>> # Track frequencies from a real spectrogram >>> y, sr = librosa.loadx('trumpet') >>> freqs, times, mags = librosa.reassigned_spectrogram(y, sr=sr, ... fill_nan=True) >>> # Select out pitches with high energy >>> freqs = freqs[mags > np.median(mags)] >>> librosa.pitch_tuning(freqs) -0.07 """ frequencies = np.atleast_1d(frequencies) # Trim out any DC components frequencies = frequencies[frequencies > 0] if not np.any(frequencies): warnings.warn( "Trying to estimate tuning from empty frequency set.", stacklevel=2 ) return 0.0 # Compute the residual relative to the number of bins residual = np.mod(bins_per_octave * convert.hz_to_octs(frequencies), 1.0) # Are we on the wrong side of the semitone? # A residual of 0.95 is more likely to be a deviation of -0.05 # from the next tone up. residual[residual >= 0.5] -= 1.0 bins = np.linspace(-0.5, 0.5, int(np.ceil(1.0 / resolution)) + 1) counts, tuning = np.histogram(residual, bins) # return the histogram peak tuning_est: float = tuning[np.argmax(counts)] return tuning_est @cache(level=30) def piptrack( *, y: np.ndarray | None = None, sr: float = 22050, S: np.ndarray | None = None, n_fft: int | None = 2048, hop_length: int | None = None, fmin: float = 150.0, fmax: float = 4000.0, threshold: float = 0.1, win_length: int | None = None, window: _WindowSpec = "hann", center: bool = True, pad_mode: _PadModeSTFT = "constant", ref: float | Callable | None = None, ) -> tuple[np.ndarray, np.ndarray]: """Pitch tracking on thresholded parabolically-interpolated STFT. This implementation uses the parabolic interpolation method described by [#]_. .. [#] https://ccrma.stanford.edu/~jos/sasp/Sinusoidal_Peak_Interpolation.html Parameters ---------- y : np.ndarray [shape=(..., n)] or None audio signal. Multi-channel is supported.. sr : number > 0 [scalar] audio sampling rate of ``y`` S : np.ndarray [shape=(..., d, t)] or None magnitude or power spectrogram n_fft : int > 0 [scalar] or None length of the FFT frame to use, if ``y`` is provided. hop_length : int > 0 [scalar] or None number of samples to hop fmin : float > 0 [scalar] lower frequency cutoff. fmax : float > 0 [scalar] upper frequency cutoff. threshold : float in `(0, 1)` A bin in spectrum ``S`` is considered a pitch when it is greater than ``threshold * ref(S)``. By default, ``ref(S)`` is taken to be ``max(S, axis=0)`` (the maximum value in each column). win_length : int <= n_fft [scalar] Each frame of audio is windowed by ``window``. The window will be of length `win_length` and then padded with zeros to match ``n_fft``. If unspecified, defaults to ``win_length = n_fft``. window : str, tuple, number, function, or np.ndarray [shape=(n_fft,)] - a window specification (string, tuple, or number); see `scipy.signal.get_window` - a window function, such as `scipy.signal.windows.hann` - a vector or array of length ``n_fft`` .. see also:: `filters.get_window` center : bool - If ``True``, the signal ``y`` is padded so that frame ``t`` is centered at ``y[t * hop_length]``. - If ``False``, then frame ``t`` begins at ``y[t * hop_length]`` pad_mode : str If ``center=True``, the padding mode to use at the edges of the signal. By default, STFT uses zero-padding. See also: `np.pad`. ref : scalar or callable [default=np.max] If scalar, the reference value against which ``S`` is compared for determining pitches. If callable, the reference value is computed as ``ref(S, axis=0)``. Returns ------- pitches, magnitudes : np.ndarray [shape=(..., d, t)] Where ``d`` is the subset of FFT bins within ``fmin`` and ``fmax``. ``pitches[..., f, t]`` contains instantaneous frequency at bin ``f``, time ``t`` ``magnitudes[..., f, t]`` contains the corresponding magnitudes. Both ``pitches`` and ``magnitudes`` take value 0 at bins of non-maximal magnitude. Notes ----- This function caches at level 30. One of ``S`` or ``y`` must be provided. If ``S`` is not given, it is computed from ``y`` using the default parameters of `librosa.stft`. Examples -------- Computing pitches from a waveform input >>> y, sr = librosa.loadx('trumpet') >>> pitches, magnitudes = librosa.piptrack(y=y, sr=sr) Or from a spectrogram input >>> S = np.abs(librosa.stft(y)) >>> pitches, magnitudes = librosa.piptrack(S=S, sr=sr) Or with an alternate reference value for pitch detection, where values above the mean spectral energy in each frame are counted as pitches >>> pitches, magnitudes = librosa.piptrack(S=S, sr=sr, threshold=1, ... ref=np.mean) """ # Check that we received an audio time series or STFT S, n_fft = _spectrogram( y=y, S=S, n_fft=n_fft, hop_length=hop_length, win_length=win_length, window=window, center=center, pad_mode=pad_mode, ) # Make sure we're dealing with magnitudes # ``np.abs(S)`` forces a full-size copy of ``S`` on every call. For a # real, non-negative spectrogram (the common case, e.g. a magnitude or # power spectrogram) that copy is redundant, so only rectify when the # input is complex or actually contains negative values. if np.iscomplexobj(S) or S.min() < 0: S = np.abs(S) # Truncate to feasible region fmin = np.maximum(fmin, 0) fmax = np.minimum(fmax, float(sr) / 2) fft_freqs = convert.fft_frequencies(sr=sr, n_fft=n_fft) # Do the parabolic interpolation everywhere, # then figure out where the peaks are # then restrict to the feasible range (fmin:fmax) avg = np.gradient(S, axis=-2) shift = _parabolic_interpolation(S, axis=-2) # this will get us the interpolated peak value dskew = 0.5 * avg * shift # Pre-allocate output pitches = np.zeros_like(S) mags = np.zeros_like(S) # Clip to the viable frequency range freq_mask = (fmin <= fft_freqs) & (fft_freqs < fmax) freq_mask = util.expand_to(freq_mask, ndim=S.ndim, axes=-2) # Compute the column-wise local max of S after thresholding # Find the argmax coordinates if ref is None: ref = np.max if callable(ref): ref_value = threshold * ref(S, axis=-2) # Reinsert the frequency axis here, in case the callable doesn't # support keepdims=True ref_value = np.expand_dims(ref_value, -2) else: ref_value = np.abs(ref) # Store pitch and magnitude idx = np.nonzero(freq_mask & util.localmax(S * (S > ref_value), axis=-2)) pitches[idx] = (idx[-2] + shift[idx]) * float(sr) / n_fft mags[idx] = S[idx] + dskew[idx] return pitches, mags def _cumulative_mean_normalized_difference( y_frames: np.ndarray, min_period: int, max_period: int, ) -> np.ndarray: """Cumulative mean normalized difference function (equation 8 in [#]_) .. [#] De Cheveigné, Alain, and Hideki Kawahara. "YIN, a fundamental frequency estimator for speech and music." The Journal of the Acoustical Society of America 111.4 (2002): 1917-1930. Parameters ---------- y_frames : np.ndarray [shape=(frame_length, n_frames)] framed audio time series. min_period : int > 0 [scalar] minimum period. max_period : int > 0 [scalar] maximum period. Returns ------- yin_frames : np.ndarray [shape=(max_period-min_period+1,n_frames)] Cumulative mean normalized difference function for each frame. """ acf_frames = audio.autocorrelate(y_frames, max_size=max_period + 1, axis=-2) # Energy terms. yin_frames = np.square(y_frames) np.cumsum(yin_frames, out=yin_frames, axis=-2) # Difference function: d(k) = 2 * (ACF(0) - ACF(k)) - sum_{m=0}^{k-1} y(m)^2 k = slice(1, max_period + 1) yin_frames[..., 0, :] = 0 yin_frames[..., k, :] = ( 2 * (acf_frames[..., 0:1, :] - acf_frames[..., k, :]) - yin_frames[..., : k.stop - 1, :] ) # Cumulative mean normalized difference function. yin_numerator = yin_frames[..., min_period : max_period + 1, :] # broadcast this shape to have leading ones k_range = util.expand_to(np.r_[k], ndim=yin_frames.ndim, axes=-2) cumulative_mean = np.cumsum(yin_frames[..., k, :], axis=-2) / k_range yin_denominator = cumulative_mean[..., min_period - 1 : max_period, :] yin_frames = yin_numerator / ( yin_denominator + util.tiny(yin_denominator) ) return np.asarray(yin_frames) @numba.stencil # type: ignore def _pi_stencil(x: np.ndarray) -> np.ndarray: """Stencil to compute local parabolic interpolation""" a = x[1] + x[-1] - 2 * x[0] b = (x[1] - x[-1]) / 2 if np.abs(b) >= np.abs(a): # If this happens, we'll shift by more than 1 bin # Suppressing types because mypy has no idea about stencils return 0 # type: ignore return -b / a # type: ignore @numba.guvectorize( "(n)->(n)", cache=True, nopython=True, ) # type: ignore def _pi_wrapper(x: np.ndarray, y: np.ndarray) -> None: # pragma: no cover """Vectorized wrapper for the parabolic interpolation stencil""" y[:] = _pi_stencil(x) def _parabolic_interpolation(x: np.ndarray, *, axis: int = -2) -> np.ndarray: """Piecewise parabolic interpolation for yin and pyin. Parameters ---------- x : np.ndarray array to interpolate axis : int axis along which to interpolate Returns ------- parabolic_shifts : np.ndarray [shape=x.shape] position of the parabola optima (relative to bin indices) Note: the shift at bin `n` is determined as 0 if the estimated optimum is outside the range `[n-1, n+1]`. """ # Rotate the target axis to the end xi = x.swapaxes(-1, axis) # Allocate the output array and rotate target axis shifts = np.empty_like(x) shiftsi = shifts.swapaxes(-1, axis) # Call the vectorized stencil _pi_wrapper(xi, shiftsi) # Handle the edge condition not covered by the stencil shiftsi[..., -1] = 0 shiftsi[..., 0] = 0 return shifts def yin( y: np.ndarray, *, fmin: float, fmax: float, sr: float = 22050, frame_length: int = 2048, hop_length: int | None = None, trough_threshold: float = 0.1, center: bool = True, pad_mode: _PadMode = "constant", ) -> np.ndarray: """Fundamental frequency (F0) estimation using the YIN algorithm. YIN is an autocorrelation based method for fundamental frequency estimation [#]_. First, a normalized difference function is computed over short (overlapping) frames of audio. Next, the first minimum in the difference function below ``trough_threshold`` is selected as an estimate of the signal's period. Finally, the estimated period is refined using parabolic interpolation before converting into the corresponding frequency. .. [#] De Cheveigné, Alain, and Hideki Kawahara. "YIN, a fundamental frequency estimator for speech and music." The Journal of the Acoustical Society of America 111.4 (2002): 1917-1930. Parameters ---------- y : np.ndarray [shape=(..., n)] audio time series. Multi-channel is supported.. fmin : number > 0 [scalar] minimum frequency in Hertz. The recommended minimum is ``librosa.note_to_hz('C2')`` (~65 Hz) though lower values may be feasible. fmax : number > fmin, <= sr/2 [scalar] maximum frequency in Hertz. The recommended maximum is ``librosa.note_to_hz('C7')`` (~2093 Hz) though higher values may be feasible. sr : number > 0 [scalar] sampling rate of ``y`` in Hertz. frame_length : int > 0 [scalar] length of the frames in samples. By default, ``frame_length=2048`` corresponds to a time scale of about 93 ms at a sampling rate of 22050 Hz. hop_length : None or int > 0 [scalar] number of audio samples between adjacent YIN predictions. If ``None``, defaults to ``frame_length // 4``. trough_threshold : number > 0 [scalar] absolute threshold for peak estimation. center : bool If ``True``, the signal `y` is padded so that frame ``D[:, t]`` is centered at `y[t * hop_length]`. If ``False``, then ``D[:, t]`` begins at ``y[t * hop_length]``. Defaults to ``True``, which simplifies the alignment of ``D`` onto a time grid by means of ``librosa.core.frames_to_samples``. pad_mode : str or function If ``center=True``, this argument is passed to ``np.pad`` for padding the edges of the signal ``y``. By default (``pad_mode="constant"``), ``y`` is padded on both sides with zeros. If ``center=False``, this argument is ignored. .. see also:: `np.pad` Returns ------- f0: np.ndarray [shape=(..., n_frames)] time series of fundamental frequencies in Hertz. If multi-channel input is provided, f0 curves are estimated separately for each channel. See Also -------- librosa.pyin : Fundamental frequency (F0) estimation using probabilistic YIN (pYIN). Examples -------- Computing a fundamental frequency (F0) curve from an audio input >>> y = librosa.chirp(fmin=440, fmax=880, duration=5.0, sr=22050) >>> librosa.yin(y, fmin=440, fmax=880, sr=22050) array([442.66354675, 441.95299983, 441.58010963, ..., 871.161732 , 873.99001454, 877.04297681]) """ if fmin is None or fmax is None: raise ParameterError('both "fmin" and "fmax" must be provided') __check_yin_params(sr=sr, fmax=fmax, fmin=fmin, frame_length=frame_length) # Set the default hop if it is not already specified. if hop_length is None: hop_length = frame_length // 4 # Check that audio is valid. util.valid_audio(y) # Pad the time series so that frames are centered if center: padding = [(0, 0)] * y.ndim padding[-1] = (frame_length // 2, frame_length // 2) y = np.pad(y, padding, mode=pad_mode) # Frame audio. y_frames = util.frame(y, frame_length=frame_length, hop_length=hop_length) # Calculate minimum and maximum periods min_period = int(np.floor(sr / fmax)) max_period = min(int(np.ceil(sr / fmin)), frame_length - 1) # Calculate cumulative mean normalized difference function. yin_frames = _cumulative_mean_normalized_difference( y_frames, min_period, max_period ) # Parabolic interpolation. parabolic_shifts = _parabolic_interpolation(yin_frames) # Find local minima. is_trough = util.localmin(yin_frames, axis=-2) is_trough[..., 0, :] = yin_frames[..., 0, :] < yin_frames[..., 1, :] # Find minima below peak threshold. is_threshold_trough = np.logical_and(is_trough, yin_frames < trough_threshold) # Absolute threshold. # "The solution we propose is to set an absolute threshold and choose the # smallest value of tau that gives a minimum of d' deeper than # this threshold. If none is found, the global minimum is chosen instead." target_shape = list(yin_frames.shape) target_shape[-2] = 1 global_min = np.argmin(yin_frames, axis=-2) yin_period = np.argmax(is_threshold_trough, axis=-2) global_min = global_min.reshape(target_shape) yin_period = yin_period.reshape(target_shape) no_trough_below_threshold = np.all(~is_threshold_trough, axis=-2, keepdims=True) yin_period[no_trough_below_threshold] = global_min[no_trough_below_threshold] # Refine peak by parabolic interpolation. yin_period = ( min_period + yin_period + np.take_along_axis(parabolic_shifts, yin_period, axis=-2) )[..., 0, :] # Convert period to fundamental frequency. f0: np.ndarray = sr / yin_period return f0 def pyin( y: np.ndarray, *, fmin: float, fmax: float, sr: float = 22050, frame_length: int = 2048, hop_length: int | None = None, n_thresholds: int = 100, beta_parameters: tuple[float, float] = (2, 18), boltzmann_parameter: float = 2, resolution: float = 0.1, max_transition_rate: float = 35.92, switch_prob: float = 0.01, no_trough_prob: float = 0.01, fill_na: float | None = np.nan, center: bool = True, pad_mode: _PadMode = "constant", transition_min_prob: float | None = 1e-4, ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: """Fundamental frequency (F0) estimation using probabilistic YIN (pYIN). pYIN [#]_ is a modificatin of the YIN algorithm [#]_ for fundamental frequency (F0) estimation. In the first step of pYIN, F0 candidates and their probabilities are computed using the YIN algorithm. In the second step, Viterbi decoding is used to estimate the most likely F0 sequence and voicing flags. .. [#] Mauch, Matthias, and Simon Dixon. "pYIN: A fundamental frequency estimator using probabilistic threshold distributions." 2014 IEEE International Conference on Acoustics, Speech and Signal Processing (ICASSP). IEEE, 2014. .. [#] De Cheveigné, Alain, and Hideki Kawahara. "YIN, a fundamental frequency estimator for speech and music." The Journal of the Acoustical Society of America 111.4 (2002): 1917-1930. Parameters ---------- y : np.ndarray [shape=(..., n)] audio time series. Multi-channel is supported. fmin : number > 0 [scalar] minimum frequency in Hertz. The recommended minimum is ``librosa.note_to_hz('C2')`` (~65 Hz) though lower values may be feasible. fmax : number > fmin, <= sr/2 [scalar] maximum frequency in Hertz. The recommended maximum is ``librosa.note_to_hz('C7')`` (~2093 Hz) though higher values may be feasible. sr : number > 0 [scalar] sampling rate of ``y`` in Hertz. frame_length : int > 0 [scalar] length of the frames in samples. By default, ``frame_length=2048`` corresponds to a time scale of about 93 ms at a sampling rate of 22050 Hz. hop_length : None or int > 0 [scalar] number of audio samples between adjacent pYIN predictions. If ``None``, defaults to ``frame_length // 4``. n_thresholds : int > 0 [scalar] number of thresholds for peak estimation. beta_parameters : tuple shape parameters for the beta distribution prior over thresholds. boltzmann_parameter : number > 0 [scalar] shape parameter for the Boltzmann distribution prior over troughs. Larger values will assign more mass to smaller periods. resolution : float in `(0, 1)` Resolution of the pitch bins. 0.01 corresponds to cents. max_transition_rate : float > 0 maximum pitch transition rate in octaves per second. switch_prob : float in ``(0, 1)`` probability of switching from voiced to unvoiced or vice versa. no_trough_prob : float in ``(0, 1)`` maximum probability to add to global minimum if no trough is below threshold. fill_na : None, float, or ``np.nan`` default value for unvoiced frames of ``f0``. If ``None``, the unvoiced frames will contain a best guess value. center : bool If ``True``, the signal ``y`` is padded so that frame ``D[:, t]`` is centered at ``y[t * hop_length]``. If ``False``, then ``D[:, t]`` begins at ``y[t * hop_length]``. Defaults to ``True``, which simplifies the alignment of ``D`` onto a time grid by means of ``librosa.core.frames_to_samples``. pad_mode : str or function If ``center=True``, this argument is passed to ``np.pad`` for padding the edges of the signal ``y``. By default (``pad_mode="constant"``), ``y`` is padded on both sides with zeros. If ``center=False``, this argument is ignored. .. see also:: `np.pad` transition_min_prob : None or float > 0 If `None`, then Viterbi decoding considers all possible predecessor states for each time step, providing an exact inference. Otherwise, only transitions with probability at least `transition_min_prob` are considered. This provides an approximate inference, but can significantly reduce computation time. Returns ------- f0 : np.ndarray [shape=(..., n_frames)] time series of fundamental frequencies in Hertz. voiced_flag : np.ndarray [shape=(..., n_frames)] time series containing boolean flags indicating whether a frame is voiced or not. voiced_prob : np.ndarray [shape=(..., n_frames)] time series containing the probability that a frame is voiced. .. note:: If multi-channel input is provided, f0 and voicing are estimated separately for each channel. See Also -------- librosa.yin : Fundamental frequency (F0) estimation using the YIN algorithm. Examples -------- Computing a fundamental frequency (F0) curve from an audio input >>> y, sr = librosa.loadx('trumpet') >>> f0, voiced_flag, voiced_probs = librosa.pyin(y, ... sr=sr, ... fmin=librosa.note_to_hz('C2'), ... fmax=librosa.note_to_hz('C7')) >>> times = librosa.times_like(f0, sr=sr) Overlay F0 over a spectrogram >>> import matplotlib.pyplot as plt >>> D = librosa.amplitude_to_db(np.abs(librosa.stft(y)), ref=np.max) >>> fig, ax = plt.subplots() >>> img = librosa.display.specshow(D, x_axis='time', y_axis='log', ax=ax) >>> ax.set(title='pYIN fundamental frequency estimation') >>> librosa.display.colorbar_db(img) >>> ax.plot(times, f0, label='f0', path_effects=librosa.display.highlight(ax=ax)) >>> ax.legend(loc='upper right') """ if fmin is None or fmax is None: raise ParameterError('both "fmin" and "fmax" must be provided') __check_yin_params(sr=sr, fmax=fmax, fmin=fmin, frame_length=frame_length) # Set the default hop if it is not already specified. if hop_length is None: hop_length = frame_length // 4 # Check that audio is valid. util.valid_audio(y) # Pad the time series so that frames are centered if center: padding = [(0, 0) for _ in y.shape] padding[-1] = (frame_length // 2, frame_length // 2) y = np.pad(y, padding, mode=pad_mode) # Frame audio. y_frames = util.frame(y, frame_length=frame_length, hop_length=hop_length) # Calculate minimum and maximum periods min_period = int(np.floor(sr / fmax)) max_period = min(int(np.ceil(sr / fmin)), frame_length - 1) # Calculate cumulative mean normalized difference function. yin_frames = _cumulative_mean_normalized_difference( y_frames, min_period, max_period ) # Parabolic interpolation. parabolic_shifts = _parabolic_interpolation(yin_frames) # Find Yin candidates and probabilities. # The implementation here follows the official pYIN software which # differs from the method described in the paper. # 1. Define the prior over the thresholds. import scipy.stats thresholds = np.linspace(0, 1, n_thresholds + 1) beta_cdf = scipy.stats.beta.cdf(thresholds, beta_parameters[0], beta_parameters[1]) beta_probs = np.diff(beta_cdf) n_bins_per_semitone = int(np.ceil(1.0 / resolution)) n_pitch_bins = int(np.floor(12 * n_bins_per_semitone * np.log2(fmax / fmin))) + 1 def _helper(a, b): return __pyin_helper( a, b, sr, thresholds, boltzmann_parameter, beta_probs, no_trough_prob, min_period, fmin, n_pitch_bins, n_bins_per_semitone, ) helper = np.vectorize(_helper, signature="(f,t),(k,t)->(1,d,t),(j,t)") observation_probs, voiced_prob = helper(yin_frames, parabolic_shifts) # Construct transition matrix. max_semitones_per_frame = round(max_transition_rate * 12 * hop_length / sr) transition_width = max_semitones_per_frame * n_bins_per_semitone + 1 # Construct the within voicing transition probabilities transition = sequence.transition_local( n_pitch_bins, transition_width, window="triangle", wrap=False ) # Include across voicing transition probabilities t_switch = sequence.transition_loop(2, 1 - switch_prob) transition = np.kron(t_switch, transition) p_init = np.ones(2 * n_pitch_bins) / (2 * n_pitch_bins) states = sequence.viterbi(observation_probs, transition, p_init=p_init, transition_min_prob=transition_min_prob) # Find f0 corresponding to each decoded pitch bin. freqs = fmin * 2 ** (np.arange(n_pitch_bins) / (12 * n_bins_per_semitone)) f0 = freqs[states % n_pitch_bins] voiced_flag = states < n_pitch_bins if fill_na is not None: f0[~voiced_flag] = fill_na return f0[..., 0, :], voiced_flag[..., 0, :], voiced_prob[..., 0, :] def __pyin_helper( yin_frames, parabolic_shifts, sr, thresholds, boltzmann_parameter, beta_probs, no_trough_prob, min_period, fmin, n_pitch_bins, n_bins_per_semitone, ): yin_probs = np.zeros_like(yin_frames) for i, yin_frame in enumerate(yin_frames.T): # 2. For each frame find the troughs. is_trough = util.localmin(yin_frame) is_trough[0] = yin_frame[0] < yin_frame[1] (trough_index,) = np.nonzero(is_trough) if len(trough_index) == 0: continue # 3. Find the troughs below each threshold. # these are the local minima of the frame, could get them directly without the trough index trough_heights = yin_frame[trough_index] trough_thresholds = np.less.outer(trough_heights, thresholds[1:]) # 4. Define the prior over the troughs. # Smaller periods are weighted more. trough_positions = np.cumsum(trough_thresholds, axis=0) - 1 n_troughs = np.count_nonzero(trough_thresholds, axis=0) import scipy.stats trough_prior = scipy.stats.boltzmann.pmf( trough_positions, boltzmann_parameter, n_troughs ) trough_prior[~trough_thresholds] = 0 # 5. For each threshold add probability to global minimum if no trough is below threshold, # else add probability to each trough below threshold biased by prior. probs = trough_prior.dot(beta_probs) global_min = np.argmin(trough_heights) n_thresholds_below_min = np.count_nonzero(~trough_thresholds[global_min, :]) probs[global_min] += no_trough_prob * np.sum( beta_probs[:n_thresholds_below_min] ) yin_probs[trough_index, i] = probs yin_period, frame_index = np.nonzero(yin_probs) # Refine peak by parabolic interpolation. period_candidates = min_period + yin_period period_candidates = period_candidates + parabolic_shifts[yin_period, frame_index] f0_candidates = sr / period_candidates # Find pitch bin corresponding to each f0 candidate. bin_index = 12 * n_bins_per_semitone * np.log2(f0_candidates / fmin) bin_index = np.clip(np.round(bin_index), 0, n_pitch_bins).astype(int) # Observation probabilities. observation_probs = np.zeros((2 * n_pitch_bins, yin_frames.shape[1])) observation_probs[bin_index, frame_index] = yin_probs[yin_period, frame_index] voiced_prob = np.clip( np.sum(observation_probs[:n_pitch_bins, :], axis=0, keepdims=True), 0, 1 ) observation_probs[n_pitch_bins:, :] = (1 - voiced_prob) / n_pitch_bins return observation_probs[np.newaxis], voiced_prob def __check_yin_params(*, sr: float, fmax: float, fmin: float, frame_length: int): """Check the feasibility of yin/pyin parameters against the following conditions: 1. 0 < fmin < fmax <= sr/2 2. sr / fmin < frame_length - 1: At least one period of fmin needs to fit into the frame. Give a warning if parameters violate: 3. sr / fmin < frame_length / 2: Ideally, at least two periods of fmin should fit into the frame. """ if fmax > sr / 2: raise ParameterError(f"fmax={fmax:.3f} cannot exceed Nyquist frequency {sr/2}") if fmin >= fmax: raise ParameterError(f"fmin={fmin:.3f} must be less than fmax={fmax:.3f}") if fmin <= 0: raise ParameterError(f"fmin={fmin:.3f} must be strictly positive") if sr / fmin >= frame_length - 1: fmin_feasible = sr / (frame_length - 1) frame_length_feasible = int(np.ceil(sr / fmin) + 1) raise ParameterError( f"fmin={fmin:.3f} is too small for frame_length={frame_length} and sr={sr}. " f"Either increase to fmin={fmin_feasible:.3f} or frame_length={frame_length_feasible}" ) if sr / fmin >= frame_length // 2: fmin_optimal = sr / (frame_length / 2) frame_length_optimal = int(np.ceil(sr / fmin) * 2 + 1) warnings.warn( f"With fmin={fmin:.3f}, sr={sr} and frame_length={frame_length}, less than two periods of fmin " f"fit into the frame, which can cause inaccurate pitch detection. " f"Consider increasing to fmin={fmin_optimal:.3f} or frame_length={frame_length_optimal}.", stacklevel=3, ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2102268 librosa-1.0.0/librosa/core/spectrum.py0000644000175100017510000031305215236640653017426 0ustar00runnerrunner#!/usr/bin/env python # -*- coding: utf-8 -*- """Utilities for spectral processing""" from __future__ import annotations import warnings from typing import TYPE_CHECKING, overload import numpy as np import scipy from numba import jit from .. import util from .._cache import cache from ..filters import get_window, semitone_filterbank, window_sumsquare from ..util.deprecation import Deprecated, rename_kw from ..util.exceptions import ParameterError from . import convert from .audio import resample if TYPE_CHECKING: from typing import Any, Callable, Literal from numpy.typing import DTypeLike from .._typing import ( RNGLike, SeedLike, _ComplexLike_co, _FloatLike_co, _InterpKind, _PadMode, _PadModeSTFT, _ScalarOrSequence, _SequenceLike, _WindowSpec, ) __all__ = [ "stft", "istft", "magphase", "iirt", "reassigned_spectrogram", "phase_vocoder", "perceptual_weighting", "power_to_db", "db_to_power", "amplitude_to_db", "db_to_amplitude", "fmt", "pcen", "griffinlim", ] @cache(level=20) def stft( y: np.ndarray, *, n_fft: int = 2048, hop_length: int | None = None, win_length: int | None = None, window: _WindowSpec = "hann", center: bool = True, dtype: DTypeLike | None = None, pad_mode: _PadModeSTFT = "constant", out: np.ndarray | None = None, ) -> np.ndarray: """Short-time Fourier transform (STFT). The STFT represents a signal in the time-frequency domain by computing discrete Fourier transforms (DFT) over short overlapping windows. This function returns a complex-valued matrix D such that - ``np.abs(D[..., f, t])`` is the magnitude of frequency bin ``f`` at frame ``t``, and - ``np.angle(D[..., f, t])`` is the phase of frequency bin ``f`` at frame ``t``. The integers ``t`` and ``f`` can be converted to physical units by means of the utility functions `frames_to_samples` and `fft_frequencies`. Parameters ---------- y : np.ndarray [shape=(..., n)], real-valued input signal. Multi-channel is supported. n_fft : int > 0 [scalar] length of the windowed signal after padding with zeros. The number of rows in the STFT matrix ``D`` is ``(1 + n_fft/2)``. The default value, ``n_fft=2048`` samples, corresponds to a physical duration of 93 milliseconds at a sample rate of 22050 Hz, i.e. the default sample rate in librosa. This value is well adapted for music signals. However, in speech processing, the recommended value is 512, corresponding to 23 milliseconds at a sample rate of 22050 Hz. In any case, we recommend setting ``n_fft`` to a power of two for optimizing the speed of the fast Fourier transform (FFT) algorithm. hop_length : int > 0 [scalar] number of audio samples between adjacent STFT columns. Smaller values increase the number of columns in ``D`` without affecting the frequency resolution of the STFT. If unspecified, defaults to ``win_length // 4`` (see below). win_length : int <= n_fft [scalar] Each frame of audio is windowed by ``window`` of length ``win_length`` and then padded with zeros to match ``n_fft``. Padding is added on both the left- and the right-side of the window so that the window is centered within the frame. Smaller values improve the temporal resolution of the STFT (i.e. the ability to discriminate impulses that are closely spaced in time) at the expense of frequency resolution (i.e. the ability to discriminate pure tones that are closely spaced in frequency). This effect is known as the time-frequency localization trade-off and needs to be adjusted according to the properties of the input signal ``y``. If unspecified, defaults to ``win_length = n_fft``. window : str, tuple, number, function, or np.ndarray [shape=(n_fft,)] Either: - a window specification (str, tuple, or number); see `scipy.signal.get_window` - a window function, such as `scipy.signal.windows.hann` - a vector or array of length ``n_fft`` Defaults to a raised cosine window (`'hann'`), which is adequate for most applications in audio signal processing. .. see also:: `filters.get_window` center : bool If ``True``, the signal ``y`` is padded so that frame ``D[:, t]`` is centered at ``y[t * hop_length]``. If ``False``, then ``D[:, t]`` begins at ``y[t * hop_length]``. Defaults to ``True``, which simplifies the alignment of ``D`` onto a time grid by means of `librosa.frames_to_samples`. Note, however, that ``center`` must be set to `False` when analyzing signals with `librosa.stream`. .. see also:: `librosa.stream` dtype : np.dtype, optional Complex numeric type for ``D``. Default is inferred to match the precision of the input signal. pad_mode : str or function If ``center=True``, this argument is passed to `np.pad` for padding the edges of the signal ``y``. By default (``pad_mode="constant"``), ``y`` is padded on both sides with zeros. .. note:: Not all padding modes supported by `numpy.pad` are supported here. `wrap`, `mean`, `maximum`, `median`, and `minimum` are not supported. Other modes that depend at most on input values at the edges of the signal (e.g., `constant`, `edge`, `linear_ramp`) are supported. If ``center=False``, this argument is ignored. .. see also:: `numpy.pad` out : np.ndarray or None A pre-allocated, complex-valued array to store the STFT results. This must be of compatible shape and dtype for the given input parameters. If `out` is larger than necessary for the provided input signal, then only a prefix slice of `out` will be used. If not provided, a new array is allocated and returned. Returns ------- D : np.ndarray [shape=(..., 1 + n_fft/2, n_frames), dtype=dtype] Complex-valued matrix of short-term Fourier transform coefficients. If a pre-allocated `out` array is provided, then `D` will be a reference to `out`. If `out` is larger than necessary, then `D` will be a sliced view: `D = out[..., :n_frames]`. See Also -------- istft : Inverse STFT reassigned_spectrogram : Time-frequency reassigned spectrogram Notes ----- This function caches at level 20. Examples -------- >>> y, sr = librosa.loadx('trumpet') >>> S = np.abs(librosa.stft(y)) >>> S array([[5.395e-03, 3.332e-03, ..., 9.862e-07, 1.201e-05], [3.244e-03, 2.690e-03, ..., 9.536e-07, 1.201e-05], ..., [7.523e-05, 3.722e-05, ..., 1.188e-04, 1.031e-03], [7.640e-05, 3.944e-05, ..., 5.180e-04, 1.346e-03]], dtype=float32) Use left-aligned frames, instead of centered frames >>> S_left = librosa.stft(y, center=False) Use a shorter hop length >>> D_short = librosa.stft(y, hop_length=64) Display a spectrogram >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots() >>> img = librosa.display.specshow(S, vscale='dBFS', ... y_axis='log', x_axis='time', ax=ax) >>> ax.set_title('Power spectrogram') >>> librosa.display.colorbar_db(img) """ # By default, use the entire frame if win_length is None: win_length = n_fft # Set the default hop, if it's not already specified if hop_length is None: hop_length = int(win_length // 4) elif not util.is_positive_int(hop_length): raise ParameterError(f"hop_length={hop_length} must be a positive integer") # Check audio is valid util.valid_audio(y) fft_window = get_window(window, win_length, fftbins=True) # Pad the window out to n_fft size fft_window = util.pad_center(fft_window, size=n_fft) # Reshape so that the window can be broadcast fft_window = util.expand_to(fft_window, ndim=1 + y.ndim, axes=-2) # Pad the time series so that frames are centered if center: if pad_mode in ("wrap", "maximum", "mean", "median", "minimum"): # Note: padding with a user-provided function "works", but # use at your own risk. # Since we don't pass-through kwargs here, any arguments # to a user-provided pad function should be encapsulated # by using functools.partial: # # >>> my_pad_func = functools.partial(pad_func, foo=x, bar=y) # >>> librosa.stft(..., pad_mode=my_pad_func) raise ParameterError( f"pad_mode='{pad_mode}' is not supported by librosa.stft" ) if n_fft > y.shape[-1]: warnings.warn( f"n_fft={n_fft} is too large for input signal of length={y.shape[-1]}", stacklevel=2 ) # Set up the padding array to be empty, and we'll fix the target dimension later padding = [(0, 0) for _ in range(y.ndim)] # How many frames depend on left padding? start_k = int(np.ceil(n_fft // 2 / hop_length)) # What's the first frame that depends on extra right-padding? tail_k = (y.shape[-1] + n_fft // 2 - n_fft) // hop_length + 1 if tail_k <= start_k: # If tail and head overlap, then just copy-pad the signal and carry on start = 0 extra = 0 padding[-1] = (n_fft // 2, n_fft // 2) y = np.pad(y, padding, mode=pad_mode) else: # If tail and head do not overlap, then we can implement padding on each part separately # and avoid a full copy-pad # "Middle" of the signal starts here, and does not depend on head padding start = start_k * hop_length - n_fft // 2 padding[-1] = (n_fft // 2, 0) # +1 here is to ensure enough samples to fill the window # fixes bug #1567 y_pre = np.pad( y[..., : (start_k - 1) * hop_length - n_fft // 2 + n_fft + 1], padding, mode=pad_mode, ) y_frames_pre = util.frame(y_pre, frame_length=n_fft, hop_length=hop_length) # Trim this down to the exact number of frames we should have y_frames_pre = y_frames_pre[..., :start_k] # How many extra frames do we have from the head? extra = y_frames_pre.shape[-1] # Determine if we have any frames that will fit inside the tail pad if tail_k * hop_length - n_fft // 2 + n_fft <= y.shape[-1] + n_fft // 2: padding[-1] = (0, n_fft // 2) y_post = np.pad( y[..., (tail_k) * hop_length - n_fft // 2 :], padding, mode=pad_mode ) y_frames_post = util.frame( y_post, frame_length=n_fft, hop_length=hop_length ) # How many extra frames do we have from the tail? extra += y_frames_post.shape[-1] else: # In this event, the first frame that touches tail padding would run off # the end of the padded array # We'll circumvent this by allocating an empty frame buffer for the tail # this keeps the subsequent logic simple post_shape = list(y_frames_pre.shape) post_shape[-1] = 0 y_frames_post = np.empty_like(y_frames_pre, shape=post_shape) else: if n_fft > y.shape[-1]: raise ParameterError( f"n_fft={n_fft} is too large for uncentered analysis of input signal of length={y.shape[-1]}" ) # "Middle" of the signal starts at sample 0 start = 0 # We have no extra frames extra = 0 if dtype is None: dtype = util.dtype_r2c(y.dtype) # Window the time series. y_frames = util.frame(y[..., start:], frame_length=n_fft, hop_length=hop_length) # Pre-allocate the STFT matrix shape = list(y_frames.shape) # This is our frequency dimension shape[-2] = 1 + n_fft // 2 # If there's padding, there will be extra head and tail frames shape[-1] += extra if out is None: stft_matrix = np.zeros(shape, dtype=dtype, order="F") elif not (np.allclose(out.shape[:-1], shape[:-1]) and out.shape[-1] >= shape[-1]): raise ParameterError( f"Shape mismatch for provided output array out.shape={out.shape} and target shape={shape}" ) elif not np.iscomplexobj(out): raise ParameterError(f"output with dtype={out.dtype} is not of complex type") else: if np.allclose(shape, out.shape): stft_matrix = out else: stft_matrix = out[..., : shape[-1]] # Fill in the warm-up if center and extra > 0: off_start = y_frames_pre.shape[-1] stft_matrix[..., :off_start] = scipy.fft.rfft(fft_window * y_frames_pre, axis=-2) off_end = y_frames_post.shape[-1] if off_end > 0: stft_matrix[..., -off_end:] = scipy.fft.rfft(fft_window * y_frames_post, axis=-2) else: off_start = 0 n_columns = int( util.MAX_MEM_BLOCK // (np.prod(y_frames.shape[:-1]) * y_frames.itemsize) ) n_columns = max(n_columns, 1) for bl_s in range(0, y_frames.shape[-1], n_columns): bl_t = min(bl_s + n_columns, y_frames.shape[-1]) stft_matrix[..., bl_s + off_start : bl_t + off_start] = scipy.fft.rfft( fft_window * y_frames[..., bl_s:bl_t], axis=-2 ) return stft_matrix @cache(level=30) def istft( stft_matrix: np.ndarray, *, hop_length: int | None = None, win_length: int | None = None, n_fft: int | None = None, window: _WindowSpec = "hann", center: bool = True, dtype: DTypeLike | None = None, length: int | None = None, out: np.ndarray | None = None, ) -> np.ndarray: """ Inverse short-time Fourier transform (ISTFT). Converts a complex-valued spectrogram ``stft_matrix`` to time-series ``y`` by minimizing the mean squared error between ``stft_matrix`` and STFT of ``y`` as described in [#]_ up to Section 2 (reconstruction from MSTFT). In general, window function, hop length and other parameters should be same as in stft, which mostly leads to perfect reconstruction of a signal from unmodified ``stft_matrix``. .. [#] D. W. Griffin and J. S. Lim, "Signal estimation from modified short-time Fourier transform," IEEE Trans. ASSP, vol.32, no.2, pp.236--243, Apr. 1984. Parameters ---------- stft_matrix : np.ndarray [shape=(..., 1 + n_fft//2, t)] STFT matrix from ``stft`` hop_length : int > 0 [scalar] Number of frames between STFT columns. If unspecified, defaults to ``win_length // 4``. win_length : int <= n_fft = 2 * (stft_matrix.shape[-2] - 1) When reconstructing the time series, each frame is windowed and each sample is normalized by the sum of squared window according to the ``window`` function (see below). If unspecified, defaults to ``n_fft``. n_fft : int > 0 or None The number of samples per frame in the input spectrogram. By default, this is inferred as ``2 * (stft_matrix.shape[-2] - 1)``, which assumes an even-length ``n_fft``. If the STFT was computed with an odd ``n_fft``, that inference is off by one; pass the original ``n_fft`` to recover the correct frame length. window : str, tuple, number, function, np.ndarray [shape=(n_fft,)] - a window specification (str, tuple, or number); see `scipy.signal.get_window` - a window function, such as `scipy.signal.windows.hann` - a user-specified window vector of length ``n_fft`` .. see also:: `filters.get_window` center : bool - If ``True``, ``D`` is assumed to have centered frames. - If ``False``, ``D`` is assumed to have left-aligned frames. dtype : numeric type Real numeric type for ``y``. Default is to match the numerical precision of the input spectrogram. length : int > 0, optional If provided, the output ``y`` is zero-padded or clipped to exactly ``length`` samples. out : np.ndarray or None A pre-allocated, complex-valued array to store the reconstructed signal ``y``. This must be of the correct shape for the given input parameters. If not provided, a new array is allocated and returned. Returns ------- y : np.ndarray [shape=(..., n)] time domain signal reconstructed from ``stft_matrix``. If ``stft_matrix`` contains more than two axes (e.g., from a stereo input signal), then ``y`` will match shape on the leading dimensions. See Also -------- stft : Short-time Fourier Transform Notes ----- This function caches at level 30. Examples -------- >>> y, sr = librosa.loadx('trumpet') >>> D = librosa.stft(y) >>> y_hat = librosa.istft(D) >>> y_hat array([-1.407e-03, -4.461e-04, ..., 5.131e-06, -1.417e-05], dtype=float32) Exactly preserving length of the input signal requires explicit padding. Otherwise, a partial frame at the end of ``y`` will not be represented. >>> n = len(y) >>> n_fft = 2048 >>> y_pad = librosa.util.fix_length(y, size=n + n_fft // 2) >>> D = librosa.stft(y_pad, n_fft=n_fft) >>> y_out = librosa.istft(D, length=n) >>> np.max(np.abs(y - y_out)) 8.940697e-08 """ if n_fft is None: n_fft = 2 * (stft_matrix.shape[-2] - 1) # By default, use the entire frame if win_length is None: win_length = n_fft # Set the default hop, if it's not already specified if hop_length is None: hop_length = int(win_length // 4) ifft_window = get_window(window, win_length, fftbins=True) # Pad out to match n_fft, and add broadcasting axes ifft_window = util.pad_center(ifft_window, size=n_fft) ifft_window = util.expand_to(ifft_window, ndim=stft_matrix.ndim, axes=-2) # For efficiency, trim STFT frames according to signal length if available if length: if center: padded_length = length + 2 * (n_fft // 2) else: padded_length = length n_frames = min(stft_matrix.shape[-1], int(np.ceil(padded_length / hop_length))) else: n_frames = stft_matrix.shape[-1] if dtype is None: dtype = util.dtype_c2r(stft_matrix.dtype) shape = list(stft_matrix.shape[:-2]) expected_signal_len = n_fft + hop_length * (n_frames - 1) if length: expected_signal_len = length elif center: expected_signal_len -= 2 * (n_fft // 2) shape.append(expected_signal_len) if out is None: y = np.zeros(shape, dtype=dtype) elif not np.allclose(out.shape, shape): raise ParameterError( f"Shape mismatch for provided output array out.shape={out.shape} != {shape}" ) else: y = out # Since we'll be doing overlap-add here, this needs to be initialized to zero. y.fill(0.0) if center: # First frame that does not depend on padding # k * hop_length - n_fft//2 >= 0 # k * hop_length >= n_fft // 2 # k >= (n_fft//2 / hop_length) start_frame = int(np.ceil((n_fft // 2) / hop_length)) # Do overlap-add on the head block ytmp = ifft_window * scipy.fft.irfft(stft_matrix[..., :start_frame], n=n_fft, axis=-2) shape[-1] = n_fft + hop_length * (start_frame - 1) head_buffer = np.zeros(shape, dtype=dtype) __overlap_add(head_buffer, ytmp, hop_length) # If y is smaller than the head buffer, take everything if y.shape[-1] < shape[-1] - n_fft // 2: y[..., :] = head_buffer[..., n_fft // 2 : y.shape[-1] + n_fft // 2] else: # Trim off the first n_fft//2 samples from the head and copy into target buffer y[..., : shape[-1] - n_fft // 2] = head_buffer[..., n_fft // 2 :] # This offset compensates for any differences between frame alignment # and padding truncation offset = start_frame * hop_length - n_fft // 2 else: start_frame = 0 offset = 0 n_columns = int( util.MAX_MEM_BLOCK // (np.prod(stft_matrix.shape[:-1]) * stft_matrix.itemsize) ) n_columns = max(n_columns, 1) frame = 0 for bl_s in range(start_frame, n_frames, n_columns): bl_t = min(bl_s + n_columns, n_frames) # invert the block and apply the window function ytmp = ifft_window * scipy.fft.irfft(stft_matrix[..., bl_s:bl_t], n=n_fft, axis=-2) # Overlap-add the istft block starting at the i'th frame __overlap_add(y[..., frame * hop_length + offset :], ytmp, hop_length) frame += bl_t - bl_s # Normalize by sum of squared window ifft_window_sum = window_sumsquare( window=window, n_frames=n_frames, win_length=win_length, n_fft=n_fft, hop_length=hop_length, dtype=dtype, ) if center: start = n_fft // 2 else: start = 0 ifft_window_sum = util.fix_length(ifft_window_sum[..., start:], size=y.shape[-1]) approx_nonzero_indices = ifft_window_sum > util.tiny(ifft_window_sum) y[..., approx_nonzero_indices] /= ifft_window_sum[approx_nonzero_indices] return y @jit(nopython=True, cache=True) def __overlap_add(y, ytmp, hop_length): # numba-accelerated overlap add for inverse stft # y is the pre-allocated output buffer # ytmp is the windowed inverse-stft frames # hop_length is the hop-length of the STFT analysis n_fft = ytmp.shape[-2] N = n_fft for frame in range(ytmp.shape[-1]): sample = frame * hop_length if N > y.shape[-1] - sample: N = y.shape[-1] - sample y[..., sample : (sample + N)] += ytmp[..., :N, frame] def __reassign_frequencies( y: np.ndarray, sr: float = 22050, S: np.ndarray | None = None, n_fft: int = 2048, hop_length: int | None = None, win_length: int | None = None, window: _WindowSpec = "hann", center: bool = True, dtype: DTypeLike | None = None, pad_mode: _PadModeSTFT = "constant", ) -> tuple[np.ndarray, np.ndarray]: """Instantaneous frequencies based on a spectrogram representation. The reassignment vector is calculated using equation 5.20 in Flandrin, Auger, & Chassande-Mottin 2002:: omega_reassigned = omega - np.imag(S_dh/S_h) where ``S_h`` is the complex STFT calculated using the original window, and ``S_dh`` is the complex STFT calculated using the derivative of the original window. See `reassigned_spectrogram` for references. It is recommended to use ``pad_mode="wrap"`` or else ``center=False``, rather than the defaults. Frequency reassignment assumes that the energy in each FFT bin is associated with exactly one signal component. Reflection padding at the edges of the signal may invalidate the reassigned estimates in the boundary frames. Parameters ---------- y : np.ndarray [shape=(..., n,)], real-valued audio time series. Multi-channel is supported. sr : number > 0 [scalar] sampling rate of ``y`` S : np.ndarray [shape=(..., d, t)] or None (optional) complex STFT calculated using the other arguments provided to `__reassign_frequencies` n_fft : int > 0 [scalar] length of the FFT frame. Defaults to 2048. hop_length : int > 0 [scalar] hop length, number samples between subsequent frames. If not supplied, defaults to ``win_length // 4``. win_length : int > 0, <= n_fft Window length. Defaults to ``n_fft``. See ``stft`` for details. window : str, tuple, number, function, or np.ndarray [shape=(n_fft,)] - a window specification (str, tuple, number); see `scipy.signal.get_window` - a window function, such as `scipy.signal.windows.hann` - a user-specified window vector of length ``n_fft`` See `stft` for details. .. see also:: `filters.get_window` center : bool - If ``True``, the signal ``y`` is padded so that frame ``S[:, t]`` is centered at ``y[t * hop_length]``. - If ``False``, then ``S[:, t]`` begins at ``y[t * hop_length]``. dtype : numeric type Complex numeric type for ``S``. Default is inferred to match the numerical precision of the input signal. pad_mode : str If ``center=True``, the padding mode to use at the edges of the signal. By default, STFT uses zero padding. Returns ------- freqs : np.ndarray [shape=(..., 1 + n_fft/2, t), dtype=real] Instantaneous frequencies: ``freqs[f, t]`` is the frequency for bin ``f``, frame ``t``. S : np.ndarray [shape=(..., 1 + n_fft/2, t), dtype=complex] Short-time Fourier transform Warns ----- RuntimeWarning Frequencies with zero support will produce a divide-by-zero warning and will be returned as `np.nan`. See Also -------- stft : Short-time Fourier Transform reassigned_spectrogram : Time-frequency reassigned spectrogram Examples -------- >>> y, sr = librosa.loadx('trumpet') >>> frequencies, S = librosa.core.spectrum.__reassign_frequencies(y, sr=sr) >>> frequencies array([[0.000e+00, 0.000e+00, ..., 0.000e+00, 0.000e+00], [3.628e+00, 4.698e+00, ..., 1.239e+01, 1.072e+01], ..., [1.101e+04, 1.102e+04, ..., 1.105e+04, 1.102e+04], [1.102e+04, 1.102e+04, ..., 1.102e+04, 1.102e+04]]) """ # retrieve window samples if needed so that the window derivative can be # calculated if win_length is None: win_length = n_fft window = get_window(window, win_length, fftbins=True) window = util.pad_center(window, size=n_fft) if S is None: if dtype is None: dtype = util.dtype_r2c(y.dtype) S_h = stft( y=y, n_fft=n_fft, hop_length=hop_length, window=window, center=center, dtype=dtype, pad_mode=pad_mode, ) else: if dtype is None: dtype = S.dtype S_h = S # cyclic gradient to correctly handle edges of a periodic window window_derivative = util.cyclic_gradient(window) S_dh = stft( y=y, n_fft=n_fft, hop_length=hop_length, window=window_derivative, center=center, dtype=dtype, pad_mode=pad_mode, ) # equation 5.20 of Flandrin, Auger, & Chassande-Mottin 2002 # the sign of the correction is reversed in some papers - see Plante, # Meyer, & Ainsworth 1998 pp. 283-284 with np.errstate(invalid="ignore"): # We can ignore divide-by-zero here because NaN is an acceptable correction value correction = -np.imag(S_dh / S_h) freqs = convert.fft_frequencies(sr=sr, n_fft=n_fft) freqs = util.expand_to(freqs, ndim=correction.ndim, axes=-2) + correction * ( 0.5 * sr / np.pi ) return freqs, S_h def __reassign_times( y: np.ndarray, sr: float = 22050, S: np.ndarray | None = None, n_fft: int = 2048, hop_length: int | None = None, win_length: int | None = None, window: _WindowSpec = "hann", center: bool = True, dtype: DTypeLike | None = None, pad_mode: _PadModeSTFT = "constant", ) -> tuple[np.ndarray, np.ndarray]: """Time reassignments based on a spectrogram representation. The reassignment vector is calculated using equation 5.23 in Flandrin, Auger, & Chassande-Mottin 2002:: t_reassigned = t + np.real(S_th/S_h) where ``S_h`` is the complex STFT calculated using the original window, and ``S_th`` is the complex STFT calculated using the original window multiplied by the time offset from the window center. See `reassigned_spectrogram` for references. It is recommended to use ``pad_mode="constant"`` (zero padding) or else ``center=False``, rather than the defaults. Time reassignment assumes that the energy in each FFT bin is associated with exactly one impulse event. Reflection padding at the edges of the signal may invalidate the reassigned estimates in the boundary frames. Parameters ---------- y : np.ndarray [shape=(..., n,)], real-valued audio time series. Multi-channel is supported. sr : number > 0 [scalar] sampling rate of ``y`` S : np.ndarray [shape=(..., d, t)] or None (optional) complex STFT calculated using the other arguments provided to `__reassign_times` n_fft : int > 0 [scalar] length of the FFT frame. Defaults to 2048. hop_length : int > 0 [scalar] hop length, number samples between subsequent frames. If not supplied, defaults to ``win_length // 4``. win_length : int > 0, <= n_fft Window length. Defaults to ``n_fft``. See `stft` for details. window : str, tuple, number, function, or np.ndarray [shape=(n_fft,)] - a window specification (str, tuple, number); see `scipy.signal.get_window` - a window function, such as `scipy.signal.windows.hann` - a user-specified window vector of length ``n_fft`` See `stft` for details. .. see also:: `filters.get_window` center : bool - If ``True``, the signal ``y`` is padded so that frame ``S[:, t]`` is centered at ``y[t * hop_length]``. - If ``False``, then ``S[:, t]`` begins at ``y[t * hop_length]``. dtype : numeric type Complex numeric type for ``S``. Default is inferred to match the precision of the input signal. pad_mode : str If ``center=True``, the padding mode to use at the edges of the signal. By default, STFT uses zero padding. Returns ------- times : np.ndarray [shape=(..., 1 + n_fft/2, t), dtype=real] Reassigned times: ``times[f, t]`` is the time for bin ``f``, frame ``t``. S : np.ndarray [shape=(..., 1 + n_fft/2, t), dtype=complex] Short-time Fourier transform Warns ----- RuntimeWarning Time estimates with zero support will produce a divide-by-zero warning and will be returned as `np.nan`. See Also -------- stft : Short-time Fourier Transform reassigned_spectrogram : Time-frequency reassigned spectrogram Examples -------- >>> y, sr = librosa.loadx('trumpet') >>> times, S = librosa.core.spectrum.__reassign_times(y, sr=sr) >>> times array([[ 2.268e-05, 1.144e-02, ..., 5.332e+00, 5.333e+00], [ 2.268e-05, 1.451e-02, ..., 5.334e+00, 5.333e+00], ..., [ 2.268e-05, -6.177e-04, ..., 5.368e+00, 5.327e+00], [ 2.268e-05, 1.420e-03, ..., 5.307e+00, 5.328e+00]]) """ # retrieve window samples if needed so that the time-weighted window can be # calculated if win_length is None: win_length = n_fft window = get_window(window, win_length, fftbins=True) window = util.pad_center(window, size=n_fft) # retrieve hop length if needed so that the frame times can be calculated if hop_length is None: hop_length = int(win_length // 4) if S is None: if dtype is None: dtype = util.dtype_r2c(y.dtype) S_h = stft( y=y, n_fft=n_fft, hop_length=hop_length, window=window, center=center, dtype=dtype, pad_mode=pad_mode, ) else: if dtype is None: dtype = S.dtype S_h = S # calculate window weighted by time half_width = n_fft // 2 window_times: np.ndarray if n_fft % 2: window_times = np.arange(-half_width, half_width + 1) else: window_times = np.arange(0.5 - half_width, half_width) window_time_weighted = window * window_times S_th = stft( y=y, n_fft=n_fft, hop_length=hop_length, window=window_time_weighted, center=center, dtype=dtype, pad_mode=pad_mode, ) # equation 5.23 of Flandrin, Auger, & Chassande-Mottin 2002 # the sign of the correction is reversed in some papers - see Plante, # Meyer, & Ainsworth 1998 pp. 283-284 with np.errstate(invalid="ignore"): # We can ignore divide-by-zero here because NaN is an acceptable correction value correction = np.real(S_th / S_h) if center: pad_length = None else: pad_length = n_fft times = convert.frames_to_time( np.arange(S_h.shape[-1]), sr=sr, hop_length=hop_length, n_fft=pad_length ) times = util.expand_to(times, ndim=correction.ndim, axes=-1) + correction / sr return times, S_h def reassigned_spectrogram( y: np.ndarray, *, sr: float = 22050, S: np.ndarray | None = None, n_fft: int = 2048, hop_length: int | None = None, win_length: int | None = None, window: _WindowSpec = "hann", center: bool = True, reassign_frequencies: bool = True, reassign_times: bool = True, ref_power: float | Callable = 1e-6, fill_nan: bool = False, clip: bool = True, dtype: DTypeLike | None = None, pad_mode: _PadModeSTFT = "constant", ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: r"""Time-frequency reassigned spectrogram. The reassignment vectors are calculated using equations 5.20 and 5.23 in [#]_:: t_reassigned = t + np.real(S_th/S_h) omega_reassigned = omega - np.imag(S_dh/S_h) where ``S_h`` is the complex STFT calculated using the original window, ``S_dh`` is the complex STFT calculated using the derivative of the original window, and ``S_th`` is the complex STFT calculated using the original window multiplied by the time offset from the window center. See [#]_ for additional algorithms, and [#]_ and [#]_ for history and discussion of the method. .. [#] Flandrin, P., Auger, F., & Chassande-Mottin, E. (2002). Time-Frequency reassignment: From principles to algorithms. In Applications in Time-Frequency Signal Processing (Vol. 10, pp. 179-204). CRC Press. .. [#] Fulop, S. A., & Fitz, K. (2006). Algorithms for computing the time-corrected instantaneous frequency (reassigned) spectrogram, with applications. The Journal of the Acoustical Society of America, 119(1), 360. doi:10.1121/1.2133000 .. [#] Auger, F., Flandrin, P., Lin, Y.-T., McLaughlin, S., Meignen, S., Oberlin, T., & Wu, H.-T. (2013). Time-Frequency Reassignment and Synchrosqueezing: An Overview. IEEE Signal Processing Magazine, 30(6), 32-41. doi:10.1109/MSP.2013.2265316 .. [#] Hainsworth, S., Macleod, M. (2003). Time-frequency reassignment: a review and analysis. Tech. Rep. CUED/FINFENG/TR.459, Cambridge University Engineering Department Parameters ---------- y : np.ndarray [shape=(..., n)], real-valued audio time series. Multi-channel is supported. sr : number > 0 [scalar] sampling rate of ``y`` S : np.ndarray [shape=(..., d, t)] or None (optional) complex STFT calculated using the other arguments provided to ``reassigned_spectrogram`` n_fft : int > 0 [scalar] length of the FFT frame. Defaults to 2048. hop_length : int > 0 [scalar] hop length, number samples between subsequent frames. If not supplied, defaults to ``win_length // 4``. win_length : int > 0, <= n_fft Window length. Defaults to ``n_fft``. See `stft` for details. window : str, tuple, number, function, or np.ndarray [shape=(n_fft,)] - a window specification (str, tuple, number); see `scipy.signal.get_window` - a window function, such as `scipy.signal.windows.hann` - a user-specified window vector of length ``n_fft`` See `stft` for details. .. see also:: `filters.get_window` center : bool - If ``True`` (default), the signal ``y`` is padded so that frame ``S[:, t]`` is centered at ``y[t * hop_length]``. See `Notes` for recommended usage in this function. - If ``False``, then ``S[:, t]`` begins at ``y[t * hop_length]``. reassign_frequencies : bool - If ``True`` (default), the returned frequencies will be instantaneous frequency estimates. - If ``False``, the returned frequencies will be a read-only view of the STFT bin frequencies for all frames. reassign_times : bool - If ``True`` (default), the returned times will be corrected (reassigned) time estimates for each bin. - If ``False``, the returned times will be a read-only view of the STFT frame times for all bins. ref_power : float >= 0 or callable Minimum power threshold for estimating time-frequency reassignments. Any bin with ``np.abs(S[f, t])**2 < ref_power`` will be returned as `np.nan` in both frequency and time, unless ``fill_nan`` is ``True``. If 0 is provided, then only bins with zero power will be returned as `np.nan` (unless ``fill_nan=True``). fill_nan : bool - If ``False`` (default), the frequency and time reassignments for bins below the power threshold provided in ``ref_power`` will be returned as `np.nan`. - If ``True``, the frequency and time reassignments for these bins will be returned as the bin center frequencies and frame times. clip : bool - If ``True`` (default), estimated frequencies outside the range `[0, 0.5 * sr]` or times outside the range `[0, len(y) / sr]` will be clipped to those ranges. - If ``False``, estimated frequencies and times beyond the bounds of the spectrogram may be returned. dtype : numeric type Complex numeric type for STFT calculation. Default is inferred to match the precision of the input signal. pad_mode : str If ``center=True``, the padding mode to use at the edges of the signal. By default, STFT uses zero padding. Returns ------- freqs, times, mags : np.ndarray [shape=(..., 1 + n_fft/2, t), dtype=real] Instantaneous frequencies: ``freqs[..., f, t]`` is the frequency for bin ``f``, frame ``t``. If ``reassign_frequencies=False``, this will instead be a read-only array of the same shape containing the bin center frequencies for all frames. Reassigned times: ``times[..., f, t]`` is the time for bin ``f``, frame ``t``. If ``reassign_times=False``, this will instead be a read-only array of the same shape containing the frame times for all bins. Magnitudes from short-time Fourier transform: ``mags[..., f, t]`` is the magnitude for bin ``f``, frame ``t``. Warns ----- RuntimeWarning Frequency or time estimates with zero support will produce a divide-by-zero warning, and will be returned as `np.nan` unless ``fill_nan=True``. See Also -------- stft : Short-time Fourier Transform Notes ----- It is recommended to use ``center=False`` with this function rather than the librosa default ``True``. Unlike ``stft``, reassigned times are not aligned to the left or center of each frame, so padding the signal does not affect the meaning of the reassigned times. However, reassignment assumes that the energy in each FFT bin is associated with exactly one signal component and impulse event. If ``reassign_times`` is ``False``, the frame times that are returned will be aligned to the left or center of the frame, depending on the value of ``center``. In this case, if ``center`` is ``True``, then ``pad_mode="wrap"`` is recommended for valid estimation of the instantaneous frequencies in the boundary frames. Examples -------- >>> import matplotlib.pyplot as plt >>> amin = 1e-10 >>> n_fft = 64 >>> sr = 4000 >>> y = 1e-3 * librosa.clicks(times=[0.3], sr=sr, click_duration=1.0, ... click_freq=1200.0, length=8000) +\ ... 1e-3 * librosa.clicks(times=[1.5], sr=sr, click_duration=0.5, ... click_freq=400.0, length=8000) +\ ... 1e-3 * librosa.chirp(fmin=200, fmax=1600, sr=sr, duration=2.0) +\ ... 1e-6 * np.random.randn(2*sr) >>> freqs, times, mags = librosa.reassigned_spectrogram(y=y, sr=sr, ... n_fft=n_fft) >>> mags_db = librosa.amplitude_to_db(mags, ref=np.max) >>> fig, ax = plt.subplots(nrows=2, sharex=True, sharey=True) >>> img = librosa.display.specshow(mags_db, x_axis="s", y_axis="linear", sr=sr, ... hop_length=n_fft//4, ax=ax[0]) >>> ax[0].set(title="Spectrogram", xlabel=None) >>> ax[0].label_outer() >>> ax[1].scatter(times, freqs, c=mags_db, cmap="magma", alpha=0.1, s=5) >>> ax[1].set_title("Reassigned spectrogram") >>> librosa.display.colorbar_db(img, ax=ax) """ if not callable(ref_power) and ref_power < 0: raise ParameterError("ref_power must be non-negative or callable.") if not reassign_frequencies and not reassign_times: raise ParameterError("reassign_frequencies or reassign_times must be True.") if win_length is None: win_length = n_fft if hop_length is None: hop_length = int(win_length // 4) # frequency and time reassignment if requested if reassign_frequencies: freqs, S = __reassign_frequencies( y=y, sr=sr, S=S, n_fft=n_fft, hop_length=hop_length, win_length=win_length, window=window, center=center, dtype=dtype, pad_mode=pad_mode, ) if reassign_times: times, S = __reassign_times( y=y, sr=sr, S=S, n_fft=n_fft, hop_length=hop_length, win_length=win_length, window=window, center=center, dtype=dtype, pad_mode=pad_mode, ) assert S is not None mags: np.ndarray = np.abs(S) # clean up reassignment issues: divide-by-zero, bins with near-zero power, # and estimates outside the spectrogram bounds # retrieve bin frequencies and frame times to replace missing estimates if fill_nan or not reassign_frequencies or not reassign_times: if center: pad_length = None else: pad_length = n_fft bin_freqs = convert.fft_frequencies(sr=sr, n_fft=n_fft) frame_times = convert.frames_to_time( frames=np.arange(S.shape[-1]), sr=sr, hop_length=hop_length, n_fft=pad_length, ) # find bins below the power threshold # reassigned bins with zero power will already be NaN if callable(ref_power): ref_p = ref_power(mags**2) else: ref_p = ref_power mags_low = np.less(mags, ref_p**0.5, where=~np.isnan(mags), out=None) # for reassigned estimates, optionally set thresholded bins to NaN, return # bin frequencies and frame times in place of NaN generated by # divide-by-zero and power threshold, and clip to spectrogram bounds if reassign_frequencies: if ref_p > 0: freqs[mags_low] = np.nan if fill_nan: freqs = np.where(np.isnan(freqs), bin_freqs[:, np.newaxis], freqs) if clip: np.clip(freqs, 0, sr / 2.0, out=freqs) # or if reassignment was not requested, return bin frequencies and frame # times for every cell is the spectrogram else: freqs = np.broadcast_to(bin_freqs[:, np.newaxis], S.shape) if reassign_times: if ref_p > 0: times[mags_low] = np.nan if fill_nan: times = np.where(np.isnan(times), frame_times[np.newaxis, :], times) if clip: np.clip(times, 0, y.shape[-1] / float(sr), out=times) else: times = np.broadcast_to(frame_times[np.newaxis, :], S.shape) return freqs, times, mags def magphase(D: np.ndarray, *, power: float = 1) -> tuple[np.ndarray, np.ndarray]: """Separate a complex-valued spectrogram D into its magnitude (S) and phase (P) components, so that ``D = S * P``. Parameters ---------- D : np.ndarray [shape=(..., d, t), dtype=complex] complex-valued spectrogram power : float > 0 Exponent for the magnitude spectrogram, e.g., 1 for energy, 2 for power, etc. Returns ------- D_mag : np.ndarray [shape=(..., d, t), dtype=real] magnitude of ``D``, raised to ``power`` D_phase : np.ndarray [shape=(..., d, t), dtype=complex] ``exp(1.j * phi)`` where ``phi`` is the phase of ``D`` Examples -------- >>> y, sr = librosa.loadx('trumpet') >>> D = librosa.stft(y) >>> magnitude, phase = librosa.magphase(D) >>> magnitude array([[5.395e-03, 3.332e-03, ..., 9.862e-07, 1.201e-05], [3.244e-03, 2.690e-03, ..., 9.536e-07, 1.201e-05], ..., [7.523e-05, 3.722e-05, ..., 1.188e-04, 1.031e-03], [7.640e-05, 3.944e-05, ..., 5.180e-04, 1.346e-03]], dtype=float32) >>> phase array([[ 1. +0.000e+00j, 1. +0.000e+00j, ..., -1. -8.742e-08j, -1. -8.742e-08j], [-1. -8.742e-08j, -0.775-6.317e-01j, ..., -0.885-4.648e-01j, 0.472-8.815e-01j], ..., [ 1. -4.342e-12j, 0.028-9.996e-01j, ..., -0.222-9.751e-01j, -0.75 -6.610e-01j], [-1. -8.742e-08j, -1. -8.742e-08j, ..., 1. +0.000e+00j, 1. +0.000e+00j]], dtype=complex64) Or get the phase angle (in radians) >>> np.angle(phase) array([[ 0.000e+00, 0.000e+00, ..., -3.142e+00, -3.142e+00], [-3.142e+00, -2.458e+00, ..., -2.658e+00, -1.079e+00], ..., [-4.342e-12, -1.543e+00, ..., -1.794e+00, -2.419e+00], [-3.142e+00, -3.142e+00, ..., 0.000e+00, 0.000e+00]], dtype=float32) """ mag = np.abs(D) # Prevent NaNs and return magnitude 0, phase 1+0j for zero zeros_to_ones = mag == 0 mag_nonzero = mag + zeros_to_ones # Compute real and imaginary separately, because complex division can # produce NaNs when denormalized numbers are involved (< ~2e-39 for # complex64, ~5e-309 for complex128) phase = np.empty_like(D, dtype=util.dtype_r2c(D.dtype)) phase.real = D.real / mag_nonzero + zeros_to_ones phase.imag = D.imag / mag_nonzero mag **= power return mag, phase def phase_vocoder( D: np.ndarray, *, rate: float | None = None, t_out: np.ndarray | None = None, kind: _InterpKind = "linear", hop_length: int | Deprecated | None = Deprecated(), n_fft: int | Deprecated | None = Deprecated(), ) -> np.ndarray: """Phase vocoder. Given an STFT matrix D, speed up by a factor of ``rate`` Based on the implementation provided by [#]_. This is a simplified implementation, intended primarily for reference and pedagogical purposes. It makes no attempt to handle transients, and is likely to produce many audible artifacts. For a higher quality implementation, we recommend the RubberBand library [#]_ and its Python wrapper `pyrubberband`. .. [#] Ellis, D. P. W. "A phase vocoder in Matlab." Columbia University, 2002. https://www.ee.columbia.edu/~dpwe/resources/matlab/pvoc/ .. [#] https://breakfastquay.com/rubberband/ Examples -------- >>> # Play at double speed >>> y, sr = librosa.loadx('trumpet') >>> D = librosa.stft(y, n_fft=2048, hop_length=512) >>> D_fast = librosa.phase_vocoder(D, rate=2.0, hop_length=512) >>> y_fast = librosa.istft(D_fast, hop_length=512) >>> # Or play at 1/3 speed >>> y, sr = librosa.loadx('trumpet') >>> D = librosa.stft(y, n_fft=2048, hop_length=512) >>> D_slow = librosa.phase_vocoder(D, rate=1./3, hop_length=512) >>> y_slow = librosa.istft(D_slow, hop_length=512) Parameters ---------- D : np.ndarray [shape=(..., n_bins, n_frames), dtype=complex] STFT matrix rate : float > 0 [scalar] Speed-up factor: ``rate > 1`` is faster, ``rate < 1`` is slower. Use this for constant-rate time-stretching. t_out : np.ndarray [shape=(n_output,)], optional Optional array of fractional input frame indices specifying the input time corresponding to each output frame. Values must lie in [0, n_frames). This can be used for variable-rate time-stretching. If given, `rate` must be None. kind : str Interpolation kind for magnitude interpolation, e.g., 'linear' or 'nearest'. See `scipy.interpolate.interp1d` for a full list of supported options. hop_length : int > 0 [scalar] or None The number of samples between successive columns of ``D``. If None, defaults to ``n_fft//4 = (D.shape[-2]-1)//2`` .. warning:: This parameter is deprecated as of 1.0 and will be removed in 1.1. It is unused in the current implementation. n_fft : int > 0 or None The number of samples per frame in D. By default (None), this will be inferred from the shape of D. However, if D was constructed using an odd-length window, the correct frame length can be specified here. .. warning:: This parameter is deprecated as of 1.0 and will be removed in 1.1. It is unused in the current implementation. Returns ------- D_stretched : np.ndarray [shape=(..., n_bins, t / rate), dtype=complex] time-stretched STFT. For variable-rate stretching, the output will have `len(t_out)` frames. See Also -------- pyrubberband """ n_frames = D.shape[-1] if not isinstance(hop_length, Deprecated): warnings.warn( "The `hop_length` parameter is deprecated as of 1.0 and will be removed in 1.1. " "It is unused in the current implementation.", FutureWarning, stacklevel=2, ) if not isinstance(n_fft, Deprecated): warnings.warn( "The `n_fft` parameter is deprecated as of 1.0 and will be removed in 1.1. " "It is unused in the current implementation.", FutureWarning, stacklevel=2, ) if (rate is None) == (t_out is None): raise ParameterError("Must specify exactly one of `rate` or `t_out`") if (rate is not None) and (rate <= 0): raise ParameterError(f"rate={rate} must be a positive number") if t_out is None: t_out = np.arange(0.0, n_frames, rate) t_out = np.asarray(t_out, dtype=float) if np.any(t_out < 0) or np.any(t_out >= n_frames): raise ParameterError("t_out values must be in the range [0, D.shape[-1])") if np.any(np.diff(t_out) < 0): warnings.warn( "t_out is not monotonic; phase estimation may be unstable", stacklevel=2 ) i0 = np.floor(t_out).astype(int) i1 = np.minimum(i0 + 1, n_frames - 1) # Unwrapped phase ph = np.angle(D) # Phase differences for each output frame diff = ph[..., i1] - ph[..., i0] # (..., n_bins, n_out) # Initialize increments: shifted version of diff phase = np.empty_like(diff) # Initialize with the first output frame phase[..., 0] = np.angle(D[..., i0[0]]) phase[..., 1:] = diff[..., :-1] # Cumulative phase; we can do this in-place to save a copy np.cumsum(phase, axis=-1, out=phase) # Interpolate magnitudes import scipy.interpolate mag_interp = scipy.interpolate.interp1d( np.arange(n_frames), np.abs(D), kind=kind, axis=-1, fill_value="extrapolate", assume_sorted=True, copy=False, ) D_stretched: np.ndarray = util.phasor(phase, mag=mag_interp(t_out)) return D_stretched @cache(level=20) def iirt( y: np.ndarray, *, sr: float = 22050, win_length: int = 2048, hop_length: int | None = None, center: bool = True, tuning: float = 0.0, pad_mode: _PadMode = "constant", flayout: Literal["ba", "sos"] = "sos", res_type: str = "soxr_hq", **kwargs: Any, ) -> np.ndarray: r"""Time-frequency representation using IIR filters This function will return a time-frequency representation using a multirate filter bank consisting of IIR filters. [#]_ First, ``y`` is resampled as needed according to the provided ``sample_rates``. Then, a filterbank with with ``n`` band-pass filters is designed. The resampled input signals are processed by the filterbank as a whole. (`scipy.signal.filtfilt` resp. `sosfiltfilt` is used to make the phase linear.) The output of the filterbank is cut into frames. For each band, the short-time mean-square power (STMSP) is calculated by summing ``win_length`` subsequent filtered time samples. When called with the default set of parameters, it will generate the TF-representation (pitch filterbank): * 85 filters with MIDI pitches [24, 108] as ``center_freqs``. * each filter having a bandwidth of one semitone. .. [#] Müller, Meinard. "Information Retrieval for Music and Motion." Springer Verlag. 2007. Parameters ---------- y : np.ndarray [shape=(..., n)] audio time series. Multi-channel is supported. sr : number > 0 [scalar] sampling rate of ``y`` win_length : int > 0 Window length. This is primarily used for centering (see below) and to simplify compatibility with other frame-based analyses (like `stft`). hop_length : int > 0 [scalar] Hop length, number samples between subsequent frames. If not supplied, defaults to ``win_length // 4``. center : bool - If ``True``, the signal ``y`` is padded so that frame ``D[..., :, t]`` is centered at ``y[t * hop_length]``. - If ``False``, then `D[..., :, t]`` begins at ``y[t * hop_length]`` tuning : float [scalar] Tuning deviation from A440 in fractions of a bin. pad_mode : str If ``center=True``, the padding mode to use at the edges of the signal. By default, this function uses zero padding. flayout : str - If `sos` (default), a series of second-order filters is used for filtering with `scipy.signal.sosfiltfilt`. Minimizes numerical precision errors for high-order filters, but is slower. - If `ba`, the standard difference equation is used for filtering with `scipy.signal.filtfilt`. Can be unstable for high-order filters. res_type : str The resampling mode. See `librosa.resample` for details. **kwargs : additional keyword arguments Additional arguments for `librosa.filters.semitone_filterbank` (e.g., could be used to provide another set of ``center_freqs`` and ``sample_rates``). Returns ------- bands_power : np.ndarray [shape=(..., n, t), dtype=dtype] Short-time mean-square power for the input signal. Raises ------ ParameterError If ``flayout`` is not None, `ba`, or `sos`. See Also -------- librosa.filters.semitone_filterbank librosa.filters.mr_frequencies librosa.cqt scipy.signal.filtfilt scipy.signal.sosfiltfilt Examples -------- >>> import matplotlib.pyplot as plt >>> y, sr = librosa.loadx('trumpet', duration=3) >>> D = np.abs(librosa.iirt(y, sr=sr)) >>> C = np.abs(librosa.cqt(y=y, sr=sr)) >>> fig, ax = plt.subplots(nrows=2, sharex=True, sharey=True) >>> img = librosa.display.specshow(C, vscale='dBFS', ... y_axis='cqt_hz', x_axis='time', ax=ax[0]) >>> ax[0].set(title='Constant-Q transform') >>> ax[0].label_outer() >>> img = librosa.display.specshow(D, vscale='dBFS', ... y_axis='cqt_hz', x_axis='time', ax=ax[1]) >>> ax[1].set_title('Semitone spectrogram (iirt)') >>> librosa.display.colorbar_db(img, ax=ax) """ if flayout not in ("ba", "sos"): raise ParameterError(f"Unsupported flayout={flayout}") # check audio input util.valid_audio(y) # Set the default hop, if it's not already specified if hop_length is None: hop_length = win_length // 4 # Pad the time series so that frames are centered if center: padding = [(0, 0) for _ in y.shape] padding[-1] = (win_length // 2, win_length // 2) y = np.pad(y, padding, mode=pad_mode) # get the semitone filterbank filterbank_ct, sample_rates = semitone_filterbank( tuning=tuning, flayout=flayout, **kwargs ) # create three downsampled versions of the audio signal y_resampled = [] y_srs = np.unique(sample_rates) for cur_sr in y_srs: y_resampled.append(resample(y, orig_sr=sr, target_sr=cur_sr, res_type=res_type)) # Compute the number of frames that will fit. The end may get truncated. n_frames = int(1 + (y.shape[-1] - win_length) // hop_length) # Pre-allocate the output array shape = list(y.shape) # Time dimension reduces to n_frames shape[-1] = n_frames # Insert a new axis at position -2 for filter response shape.insert(-1, len(filterbank_ct)) bands_power = np.empty_like(y, shape=shape) slices: list[int | slice] = [slice(None) for _ in bands_power.shape] import scipy.signal for i, (cur_sr, cur_filter) in enumerate(zip(sample_rates, filterbank_ct, strict=True)): slices[-2] = i # filter the signal cur_sr_idx = np.flatnonzero(y_srs == cur_sr)[0] if flayout == "ba": cur_filter_output = scipy.signal.filtfilt( cur_filter[0], cur_filter[1], y_resampled[cur_sr_idx], axis=-1 ) elif flayout == "sos": cur_filter_output = scipy.signal.sosfiltfilt( cur_filter, y_resampled[cur_sr_idx], axis=-1 ) factor = sr / cur_sr hop_length_STMSP = hop_length / factor win_length_STMSP_round = round(win_length / factor) # hop_length_STMSP is used here as a floating-point number. # The discretization happens at the end to avoid accumulated rounding errors. start_idx = np.arange( 0, cur_filter_output.shape[-1] - win_length_STMSP_round, hop_length_STMSP ) if len(start_idx) < n_frames: min_length = ( int(np.ceil(n_frames * hop_length_STMSP)) + win_length_STMSP_round ) cur_filter_output = util.fix_length(cur_filter_output, size=min_length) start_idx = np.arange( 0, cur_filter_output.shape[-1] - win_length_STMSP_round, hop_length_STMSP, ) start_idx = np.round(start_idx).astype(int)[:n_frames] idx = np.add.outer(start_idx, np.arange(win_length_STMSP_round)) bands_power[tuple(slices)] = factor * np.sum( cur_filter_output[..., idx] ** 2, axis=-1 ) return bands_power @overload def power_to_db( S: _ComplexLike_co, *, ref: float | Callable = ..., amin: float = ..., top_db: float | None = ..., axes: Literal["auto"] | int | tuple[int, ...] | None = ..., ) -> np.floating: ... @overload def power_to_db( S: _SequenceLike[_ComplexLike_co], *, ref: float | Callable = ..., amin: float = ..., top_db: float | None = ..., axes: Literal["auto"] | int | tuple[int, ...] | None = ..., ) -> np.ndarray: ... @cache(level=30) def power_to_db( S: _ScalarOrSequence[_ComplexLike_co], *, ref: float | Callable = 1.0, amin: float = 1e-10, top_db: float | None = 80.0, axes: Literal["auto"] | int | tuple[int, ...] | None = "auto", ) -> np.floating | np.ndarray: """Convert a power spectrogram (amplitude squared) to decibel (dB) units This computes the scaling ``10 * log10(S / ref)`` in a numerically stable way. Parameters ---------- S : np.ndarray input power ref : scalar or callable If scalar, the amplitude ``abs(S)`` is scaled relative to ``ref``:: 10 * log10(S / ref) Zeros in the output correspond to positions where ``S == ref``. If callable, the reference value is computed as ``ref(S, axis=axes, keepdims=True)``. amin : float > 0 [scalar] minimum threshold for ``abs(S)`` and ``ref`` top_db : float >= 0 [scalar] threshold the output at ``top_db`` below the peak: ``max(10 * log10(S/ref)) - top_db`` For multi-channel inputs, with `axes='auto'`, peaks are calculated independently for each channel. axes: None, "auto", int, or tuple of int Axis or axes along which to compute the reference value (if `ref` is callable). If `auto`, then axes will be inferred as the trailing dimensions of `S`: - If `S` is scalar, then `axes=None` - If `S` is 1D, then `axes=(-1,)` - If `S` is >=2D, then `axes=(-2, -1)` Returns ------- S_db : np.ndarray ``S_db ~= 10 * log10(S) - 10 * log10(ref)`` See Also -------- perceptual_weighting db_to_power amplitude_to_db db_to_amplitude Notes ----- This function caches at level 30. Examples -------- Get a power spectrogram from a waveform ``y`` >>> y, sr = librosa.loadx('trumpet') >>> S = np.abs(librosa.stft(y)) >>> librosa.power_to_db(S**2) array([[-41.809, -41.809, ..., -41.809, -41.809], [-41.809, -41.809, ..., -41.809, -41.809], ..., [-41.809, -41.809, ..., -41.809, -41.809], [-41.809, -41.809, ..., -41.809, -41.809]], dtype=float32) Compute dB relative to peak power >>> librosa.power_to_db(S**2, ref=np.max) array([[-80., -80., ..., -80., -80.], [-80., -80., ..., -80., -80.], ..., [-80., -80., ..., -80., -80.], [-80., -80., ..., -80., -80.]], dtype=float32) Or compare to median power >>> librosa.power_to_db(S**2, ref=np.median) array([[16.578, 16.578, ..., 16.578, 16.578], [16.578, 16.578, ..., 16.578, 16.578], ..., [16.578, 16.578, ..., 16.578, 16.578], [16.578, 16.578, ..., 16.578, 16.578]], dtype=float32) And plot the results >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=2, sharex=True, sharey=True) >>> imgpow = librosa.display.specshow(S**2, sr=sr, y_axis='log', x_axis='time', ... ax=ax[0]) >>> ax[0].set(title='Power spectrogram') >>> ax[0].label_outer() >>> imgdb = librosa.display.specshow(librosa.power_to_db(S**2, ref=np.max), ... sr=sr, y_axis='log', x_axis='time', ax=ax[1]) >>> ax[1].set(title='Log-Power spectrogram') >>> fig.colorbar(imgpow, ax=ax[0]) >>> librosa.display.colorbar_db(imgdb, ax=ax[1]) """ S = np.asarray(S) if amin <= 0: raise ParameterError("amin must be strictly positive") if np.issubdtype(S.dtype, np.complexfloating): warnings.warn( "power_to_db was called on complex input so phase " "information will be discarded. To suppress this warning, " "call power_to_db(np.abs(D)**2) instead.", stacklevel=2, ) magnitude = np.abs(S) else: magnitude = S if axes == "auto": if magnitude.ndim >= 2: axes = (-2, -1) elif magnitude.ndim == 1: axes = (-1,) else: axes = None if callable(ref): try: # Compute the reference value along the specified axes, keeping dimensions for broadcasting ref_value = ref(magnitude, axis=axes, keepdims=True) except TypeError as e: raise ParameterError( "The provided reference function must support 'axis' and " "'keepdims' arguments for proper multichannel processing." ) from e else: ref_value = np.abs(ref) log_spec: np.ndarray = 10.0 * np.log10(np.maximum(amin, magnitude)) log_spec -= 10.0 * np.log10(np.maximum(amin, ref_value)) if top_db is not None: if top_db < 0: raise ParameterError("top_db must be non-negative") log_spec = np.maximum(log_spec, log_spec.max(axis=axes, keepdims=True) - top_db) return log_spec[()] @overload def db_to_power( S_db: _FloatLike_co, *, ref: float = ..., ) -> np.floating: ... @overload def db_to_power( S_db: np.ndarray, *, ref: float = ..., ) -> np.ndarray: ... @cache(level=30) def db_to_power( S_db: _FloatLike_co | np.ndarray, *, ref: float = 1.0 ) -> np.floating | np.ndarray: """Convert dB-scale values to a power values. This effectively inverts ``power_to_db``:: db_to_power(S_db) ~= ref * 10.0**(S_db / 10) Parameters ---------- S_db : np.ndarray dB-scaled values ref : number > 0 Reference power: output will be scaled by this value Returns ------- S : np.ndarray Power values Notes ----- This function caches at level 30. """ return ref * np.power(10.0, S_db * 0.1) @overload def amplitude_to_db( S: _ComplexLike_co, *, ref: float | Callable = ..., amin: float = ..., top_db: float | None = ..., axes: Literal["auto"] | int | tuple[int, ...] | None = ..., ) -> np.floating: ... @overload def amplitude_to_db( S: _SequenceLike[_ComplexLike_co], *, ref: float | Callable = ..., amin: float = ..., top_db: float | None = ..., axes: Literal["auto"] | int | tuple[int, ...] | None = ..., ) -> np.ndarray: ... @cache(level=30) def amplitude_to_db( S: _ScalarOrSequence[_ComplexLike_co], *, ref: float | Callable = 1.0, amin: float = 1e-5, top_db: float | None = 80.0, axes: Literal["auto"] | int | tuple[int, ...] | None = "auto", ) -> np.floating | np.ndarray: """Convert an amplitude spectrogram to dB-scaled spectrogram. This is equivalent to ``power_to_db(S**2, ref=ref**2, amin=amin**2, top_db=top_db)``, but is provided for convenience. Parameters ---------- S : np.ndarray input amplitude ref : scalar or callable If scalar, the amplitude ``abs(S)`` is scaled relative to ``ref``: ``20 * log10(S / ref)``. Zeros in the output correspond to positions where ``S == ref``. If callable, the reference value is computed as ``ref(S, axis=axes, keepdims=True)``. amin : float > 0 [scalar] minimum threshold for ``S`` and ``ref`` top_db : float >= 0 [scalar] threshold the output at ``top_db`` below the peak: ``max(20 * log10(S/ref)) - top_db`` For multi-channel inputs, with `axes='auto'`, peaks are calculated independently for each channel. axes : None, "auto", int, or tuple of int Axis or axes along which to compute the reference value (if `ref` is callable). If `auto`, then axes will be inferred as the trailing dimensions of `S`: - If `S` is scalar, then `axes=None` - If `S` is 1D, then `axes=(-1,)` - If `S` is >=2D, then `axes=(-2, -1)` Returns ------- S_db : np.ndarray ``S`` measured in dB See Also -------- power_to_db, db_to_amplitude Notes ----- This function caches at level 30. """ S = np.asarray(S) if np.issubdtype(S.dtype, np.complexfloating): warnings.warn( "amplitude_to_db was called on complex input so phase " "information will be discarded. To suppress this warning, " "call amplitude_to_db(np.abs(S)) instead.", stacklevel=2, ) magnitude = np.abs(S) if axes == "auto": if magnitude.ndim >= 2: axes = (-2, -1) elif magnitude.ndim == 1: axes = (-1,) else: axes = None if callable(ref): try: # Compute the reference value along the specified axes, keeping dimensions for broadcasting ref_value = ref(magnitude, axis=axes, keepdims=True) except TypeError as e: raise ParameterError( "The provided reference function must support 'axis' and " "'keepdims' arguments for proper multichannel processing." ) from e else: ref_value = np.abs(ref) out_array = magnitude if isinstance(magnitude, np.ndarray) else None power = np.square(magnitude, out=out_array) db: np.ndarray = power_to_db(power, ref=ref_value**2, amin=amin**2, top_db=top_db, axes=axes) return db[()] @overload def db_to_amplitude( S_db: _FloatLike_co, *, ref: float = ..., ) -> np.floating: ... @overload def db_to_amplitude( S_db: np.ndarray, *, ref: float = ..., ) -> np.ndarray: ... @cache(level=30) def db_to_amplitude( S_db: _FloatLike_co | np.ndarray, *, ref: float = 1.0 ) -> np.floating | np.ndarray: """Convert a dB-scaled spectrogram to an amplitude spectrogram. This effectively inverts `amplitude_to_db`:: db_to_amplitude(S_db) ~= 10.0**(0.5 * S_db/10 + log10(ref)) Parameters ---------- S_db : np.ndarray dB-scaled values ref : number > 0 Optional reference amplitude. Returns ------- S : np.ndarray Linear magnitude values Notes ----- This function caches at level 30. """ return db_to_power(S_db, ref=ref**2) ** 0.5 @cache(level=30) def perceptual_weighting( S: np.ndarray, frequencies: np.ndarray, *, kind: str = "A", **kwargs: Any ) -> np.ndarray: """Perceptual weighting of a power spectrogram:: S_p[..., f, :] = frequency_weighting(f, 'A') + 10*log(S[..., f, :] / ref) Parameters ---------- S : np.ndarray [shape=(..., d, t)] Power spectrogram frequencies : np.ndarray [shape=(d,)] Center frequency for each row of` `S`` kind : str The frequency weighting curve to use. e.g. `'A'`, `'B'`, `'C'`, `'D'`, `None or 'Z'` **kwargs : additional keyword arguments Additional keyword arguments to `power_to_db`. Returns ------- S_p : np.ndarray [shape=(..., d, t)] perceptually weighted version of ``S`` See Also -------- power_to_db Notes ----- This function caches at level 30. Examples -------- Re-weight a CQT power spectrum, using peak power as reference >>> y, sr = librosa.loadx('trumpet') >>> C = np.abs(librosa.cqt(y, sr=sr, fmin=librosa.note_to_hz('A1'))) >>> freqs = librosa.cqt_frequencies(C.shape[0], ... fmin=librosa.note_to_hz('A1')) >>> perceptual_CQT = librosa.perceptual_weighting(C**2, ... freqs, ... ref=np.max) >>> perceptual_CQT array([[ -96.528, -97.101, ..., -108.561, -108.561], [ -95.88 , -96.479, ..., -107.551, -107.551], ..., [ -65.142, -53.256, ..., -80.098, -80.098], [ -71.542, -53.197, ..., -80.311, -80.311]]) >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=2, sharex=True, sharey=True) >>> img = librosa.display.specshow(librosa.amplitude_to_db(C, ... ref=np.max), ... fmin=librosa.note_to_hz('A1'), ... y_axis='cqt_hz', x_axis='time', ... ax=ax[0]) >>> ax[0].set(title='Log CQT power') >>> ax[0].label_outer() >>> imgp = librosa.display.specshow(perceptual_CQT, y_axis='cqt_hz', ... fmin=librosa.note_to_hz('A1'), ... x_axis='time', ax=ax[1]) >>> ax[1].set(title='Perceptually weighted log CQT') >>> librosa.display.colorbar_db(img, ax=ax[0]) >>> librosa.display.colorbar_db(imgp, ax=ax[1]) """ offset = convert.frequency_weighting(frequencies, kind=kind).reshape((-1, 1)) result: np.ndarray = offset + power_to_db(S, **kwargs) return result @cache(level=30) def fmt( y: np.ndarray, *, t_min: float = 0.5, n_fmt: int | None = None, kind: _InterpKind = "cubic", beta: float = 0.5, over_sample: float = 1, axis: int = -1, ) -> np.ndarray: """Fast Mellin transform (FMT) The Mellin of a signal `y` is performed by interpolating `y` on an exponential time axis, applying a polynomial window, and then taking the discrete Fourier transform. [#]_ When the Mellin parameter (beta) is 1/2, it is also known as the scale transform. [#]_ The scale transform can be useful for audio analysis because its magnitude is invariant to scaling of the domain (e.g., time stretching or compression). This is analogous to the magnitude of the Fourier transform being invariant to shifts in the input domain. .. [#] De Sena, Antonio, and Davide Rocchesso. "A fast Mellin and scale transform." EURASIP Journal on Applied Signal Processing 2007.1 (2007): 75-75. .. [#] Cohen, L. "The scale representation." IEEE Transactions on Signal Processing 41, no. 12 (1993): 3275-3292. Parameters ---------- y : np.ndarray, real-valued The input signal(s). Can be multidimensional. The target axis must contain at least 3 samples. t_min : float > 0 The minimum time spacing (in samples). This value should generally be less than 1 to preserve as much information as possible. n_fmt : int > 2 or None The number of scale transform bins to use. If None, then ``n_bins = over_sample * ceil(n * log((n-1)/t_min))`` is taken, where ``n = y.shape[axis]`` kind : str The type of interpolation to use when re-sampling the input. See `scipy.interpolate.interp1d` for possible values. Note that the default is to use high-precision (cubic) interpolation. This can be slow in practice; if speed is preferred over accuracy, then consider using ``kind='linear'``. beta : float The Mellin parameter. ``beta=0.5`` provides the scale transform. over_sample : float >= 1 Over-sampling factor for exponential resampling. axis : int The axis along which to transform ``y`` Returns ------- x_scale : np.ndarray [dtype=complex] The scale transform of ``y`` along the ``axis`` dimension. Raises ------ ParameterError if ``n_fmt < 2`` or ``t_min <= 0`` or if ``y`` is not finite or if ``y.shape[axis] < 3``. Notes ----- This function caches at level 30. Examples -------- >>> # Generate a signal and time-stretch it (with energy normalization) >>> scale = 1.25 >>> freq = 3.0 >>> x1 = np.linspace(0, 1, num=1024, endpoint=False) >>> x2 = np.linspace(0, 1, num=int(scale * len(x1)), endpoint=False) >>> y1 = np.sin(2 * np.pi * freq * x1) >>> y2 = np.sin(2 * np.pi * freq * x2) / np.sqrt(scale) >>> # Verify that the two signals have the same energy >>> np.sum(np.abs(y1)**2), np.sum(np.abs(y2)**2) (255.99999999999997, 255.99999999999969) >>> scale1 = librosa.fmt(y1, n_fmt=512) >>> scale2 = librosa.fmt(y2, n_fmt=512) >>> # And plot the results >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=2) >>> ax[0].plot(y1, label='Original') >>> ax[0].plot(y2, linestyle='--', label='Stretched') >>> ax[0].set(xlabel='time (samples)', title='Input signals') >>> ax[0].legend() >>> ax[1].semilogy(np.abs(scale1), label='Original') >>> ax[1].semilogy(np.abs(scale2), linestyle='--', label='Stretched') >>> ax[1].set(xlabel='scale coefficients', title='Scale transform magnitude') >>> ax[1].legend() >>> plt.show() >>> # Plot the scale transform of an onset strength autocorrelation >>> y, sr = librosa.loadx('choice') >>> odf = librosa.onset.onset_strength(y=y, sr=sr) >>> # Auto-correlate with up to 10 seconds lag >>> odf_ac = librosa.autocorrelate(odf, max_size=10 * sr // 512) >>> # Normalize >>> odf_ac_norm = librosa.util.normalize(odf_ac, norm=np.inf) >>> # Compute the scale transform >>> odf_ac_scale = librosa.fmt(odf_ac_norm, n_fmt=512) >>> # Plot the results >>> fig, ax = plt.subplots(nrows=3, layout='compressed') >>> ax[0].plot(odf, label='Onset strength') >>> ax[0].set(xlabel='Time (frames)', title='Onset strength') >>> ax[1].plot(odf_ac_norm, label='Onset autocorrelation') >>> ax[1].set(xlabel='Lag (frames)', title='Onset autocorrelation') >>> ax[2].semilogy(np.abs(odf_ac_scale), label='Scale transform magnitude') >>> ax[2].set(xlabel='scale coefficients') >>> plt.show() """ n = y.shape[axis] if n < 3: raise ParameterError(f"y.shape[{axis}]=={n} < 3") if t_min <= 0: raise ParameterError(f"t_min={t_min} must be a positive number") if n_fmt is None: if over_sample < 1: raise ParameterError(f"over_sample={over_sample} must be >= 1") # The base is the maximum ratio between adjacent samples # Since the sample spacing is increasing, this is simply the # ratio between the positions of the last two samples: (n-1)/(n-2) log_base = np.log(n - 1) - np.log(n - 2) n_fmt = int(np.ceil(over_sample * (np.log(n - 1) - np.log(t_min)) / log_base)) elif n_fmt < 3: raise ParameterError(f"n_fmt=={n_fmt} < 3") else: log_base = (np.log(n_fmt - 1) - np.log(n_fmt - 2)) / over_sample if not np.all(np.isfinite(y)): raise ParameterError("y must be finite everywhere") base = np.exp(log_base) # original grid: signal covers [0, 1). This range is arbitrary, but convenient. # The final sample is positioned at (n-1)/n, so we omit the endpoint x = np.linspace(0, 1, num=n, endpoint=False) # build the interpolator import scipy.interpolate f_interp = scipy.interpolate.interp1d(x, y, kind=kind, axis=axis) # build the new sampling grid # exponentially spaced between t_min/n and 1 (exclusive) # we'll go one past where we need, and drop the last sample # When over-sampling, the last input sample contributions n_over samples. # To keep the spacing consistent, we over-sample by n_over, and then # trim the final samples. n_over = int(np.ceil(over_sample)) x_exp = np.logspace( (np.log(t_min) - np.log(n)) / log_base, 0, num=n_fmt + n_over, endpoint=False, base=base, )[:-n_over] # Clean up any rounding errors at the boundaries of the interpolation # The interpolator gets angry if we try to extrapolate, so clipping is necessary here. if x_exp[0] < t_min or x_exp[-1] > float(n - 1.0) / n: x_exp = np.clip(x_exp, float(t_min) / n, x[-1]) # Make sure that all sample points are unique # This should never happen! if len(np.unique(x_exp)) != len(x_exp): raise ParameterError("Redundant sample positions in Mellin transform") # Resample the signal y_res = f_interp(x_exp) # Broadcast the window correctly shape = [1] * y_res.ndim shape[axis] = -1 # Apply the window and fft # Normalization is absorbed into the window here for expedience result: np.ndarray = scipy.fft.rfft( y_res * ((x_exp**beta).reshape(shape) * np.sqrt(n) / n_fmt), axis=axis ) return result @overload def pcen( S: np.ndarray, *, sr: float = ..., hop_length: int = ..., gain: float = ..., bias: float = ..., power: float = ..., time_constant: float = ..., eps: float = ..., b: float | None = ..., max_size: int = ..., ref: np.ndarray | None = ..., axis: int = ..., max_axis: int | None = ..., zi: np.ndarray | None = ..., return_zf: Literal[False] = ..., ) -> np.ndarray: ... @overload def pcen( S: np.ndarray, *, sr: float = ..., hop_length: int = ..., gain: float = ..., bias: float = ..., power: float = ..., time_constant: float = ..., eps: float = ..., b: float | None = ..., max_size: int = ..., ref: np.ndarray | None = ..., axis: int = ..., max_axis: int | None = ..., zi: np.ndarray | None = ..., return_zf: Literal[True], ) -> tuple[np.ndarray, np.ndarray]: ... @cache(level=30) def pcen( S: np.ndarray, *, sr: float = 22050, hop_length: int = 512, gain: float = 0.98, bias: float = 2, power: float = 0.5, time_constant: float = 0.400, eps: float = 1e-6, b: float | None = None, max_size: int = 1, ref: np.ndarray | None = None, axis: int = -1, max_axis: int | None = None, zi: np.ndarray | None = None, return_zf: bool = False, ) -> np.ndarray | tuple[np.ndarray, np.ndarray]: """Per-channel energy normalization (PCEN) This function normalizes a time-frequency representation ``S`` by performing automatic gain control, followed by nonlinear compression [#]_ :: P[f, t] = (S / (eps + M[f, t])**gain + bias)**power - bias**power IMPORTANT: the default values of eps, gain, bias, and power match the original publication, in which ``S`` is a 40-band mel-frequency spectrogram with 25 ms windowing, 10 ms frame shift, and raw audio values in the interval [-2**31; 2**31-1[. If you use these default values, we recommend to make sure that the raw audio is properly scaled to this interval, and not to [-1, 1[ as is most often the case. The matrix ``M`` is the result of applying a low-pass, temporal IIR filter to ``S``:: M[f, t] = (1 - b) * M[f, t - 1] + b * S[f, t] If ``b`` is not provided, it is calculated as:: b = (sqrt(1 + 4* T**2) - 1) / (2 * T**2) where ``T = time_constant * sr / hop_length``. [#]_ This normalization is designed to suppress background noise and emphasize foreground signals, and can be used as an alternative to decibel scaling (`amplitude_to_db`). This implementation also supports smoothing across frequency bins by specifying ``max_size > 1``. If this option is used, the filtered spectrogram ``M`` is computed as:: M[f, t] = (1 - b) * M[f, t - 1] + b * R[f, t] where ``R`` has been max-filtered along the frequency axis, similar to the SuperFlux algorithm implemented in `onset.onset_strength`:: R[f, t] = max(S[f - max_size//2: f + max_size//2, t]) This can be used to perform automatic gain control on signals that cross or span multiple frequency bans, which may be desirable for spectrograms with high frequency resolution. .. [#] Wang, Y., Getreuer, P., Hughes, T., Lyon, R. F., & Saurous, R. A. (2017, March). Trainable frontend for robust and far-field keyword spotting. In Acoustics, Speech and Signal Processing (ICASSP), 2017 IEEE International Conference on (pp. 5670-5674). IEEE. .. [#] Lostanlen, V., Salamon, J., McFee, B., Cartwright, M., Farnsworth, A., Kelling, S., and Bello, J. P. Per-Channel Energy Normalization: Why and How. IEEE Signal Processing Letters, 26(1), 39-43. Parameters ---------- S : np.ndarray (non-negative) The input (magnitude) spectrogram sr : number > 0 [scalar] The audio sampling rate hop_length : int > 0 [scalar] The hop length of ``S``, expressed in samples gain : number >= 0 [scalar] The gain factor. Typical values should be slightly less than 1. bias : number >= 0 [scalar] The bias point of the nonlinear compression (default: 2) power : number >= 0 [scalar] The compression exponent. Typical values should be between 0 and 0.5. Smaller values of ``power`` result in stronger compression. At the limit ``power=0``, polynomial compression becomes logarithmic. time_constant : number > 0 [scalar] The time constant for IIR filtering, measured in seconds. eps : number > 0 [scalar] A small constant used to ensure numerical stability of the filter. b : number in [0, 1] [scalar] The filter coefficient for the low-pass filter. If not provided, it will be inferred from ``time_constant``. max_size : int > 0 [scalar] The width of the max filter applied to the frequency axis. If left as `1`, no filtering is performed. ref : None or np.ndarray (shape=S.shape) An optional pre-computed reference spectrum (``R`` in the above). If not provided it will be computed from ``S``. axis : int [scalar] The (time) axis of the input spectrogram. max_axis : None or int [scalar] The frequency axis of the input spectrogram. If `None`, and ``S`` is two-dimensional, it will be inferred as the opposite from ``axis``. If ``S`` is not two-dimensional, and ``max_size > 1``, an error will be raised. zi : np.ndarray The initial filter delay values. This may be the ``zf`` (final delay values) of a previous call to ``pcen``, or computed by `scipy.signal.lfilter_zi`. return_zf : bool If ``True``, return the final filter delay values along with the PCEN output ``P``. This is primarily useful in streaming contexts, where the final state of one block of processing should be used to initialize the next block. If ``False`` (default) only the PCEN values ``P`` are returned. Returns ------- P : np.ndarray, non-negative [shape=(n, m)] The per-channel energy normalized version of ``S``. zf : np.ndarray (optional) The final filter delay values. Only returned if ``return_zf=True``. See Also -------- amplitude_to_db librosa.onset.onset_strength Examples -------- Compare PCEN to log amplitude (dB) scaling on Mel spectra >>> import matplotlib.pyplot as plt >>> y, sr = librosa.loadx('robin') >>> # We recommend scaling y to the range [-2**31, 2**31[ before applying >>> # PCEN's default parameters. Furthermore, we use power=1 to get a >>> # magnitude spectrum instead of a power spectrum. >>> S = librosa.feature.melspectrogram(y=y, sr=sr, power=1) >>> log_S = librosa.amplitude_to_db(S, ref=np.max) >>> pcen_S = librosa.pcen(S * (2**31)) >>> fig, ax = plt.subplots(nrows=2, sharex=True, sharey=True) >>> img = librosa.display.specshow(log_S, x_axis='time', y_axis='mel', ax=ax[0]) >>> ax[0].set(title='log amplitude (dB)', xlabel=None) >>> ax[0].label_outer() >>> imgpcen = librosa.display.specshow(pcen_S, x_axis='time', y_axis='mel', ax=ax[1]) >>> ax[1].set(title='Per-channel energy normalization') >>> librosa.display.colorbar_db(img, ax=ax[0]) >>> fig.colorbar(imgpcen, ax=ax[1]) >>> plt.show() Compare PCEN with and without max-filtering >>> pcen_max = librosa.pcen(S * (2**31), max_size=3) >>> fig, ax = plt.subplots(nrows=2, sharex=True, sharey=True) >>> librosa.display.specshow(pcen_S, x_axis='time', y_axis='mel', ax=ax[0]) >>> ax[0].set(title='Per-channel energy normalization (no max-filter)') >>> ax[0].label_outer() >>> img = librosa.display.specshow(pcen_max, x_axis='time', y_axis='mel', ax=ax[1]) >>> ax[1].set(title='Per-channel energy normalization (max_size=3)') >>> fig.colorbar(img, ax=ax) >>> plt.show() """ if power < 0: raise ParameterError(f"power={power} must be nonnegative") if gain < 0: raise ParameterError(f"gain={gain} must be non-negative") if bias < 0: raise ParameterError(f"bias={bias} must be non-negative") if eps <= 0: raise ParameterError(f"eps={eps} must be strictly positive") if time_constant <= 0: raise ParameterError(f"time_constant={time_constant} must be strictly positive") if not util.is_positive_int(max_size): raise ParameterError(f"max_size={max_size} must be a positive integer") if b is None: t_frames = time_constant * sr / float(hop_length) # By default, this solves the equation for b: # b**2 + (1 - b) / t_frames - 2 = 0 # which approximates the full-width half-max of the # squared frequency response of the IIR low-pass filter b = (np.sqrt(1 + 4 * t_frames**2) - 1) / (2 * t_frames**2) if not 0 <= b <= 1: raise ParameterError(f"b={b} must be between 0 and 1") if np.issubdtype(S.dtype, np.complexfloating): warnings.warn( "pcen was called on complex input so phase " "information will be discarded. To suppress this warning, " "call pcen(np.abs(D)) instead.", stacklevel=2, ) S = np.abs(S) if ref is None: if max_size == 1: ref = S elif S.ndim == 1: raise ParameterError( "Max-filtering cannot be applied to 1-dimensional input" ) else: if max_axis is None: if S.ndim != 2: raise ParameterError( f"Max-filtering a {S.ndim:d}-dimensional spectrogram " "requires you to specify max_axis" ) # if axis = 0, max_axis=1 # if axis = +- 1, max_axis = 0 max_axis = np.mod(1 - axis, 2) import scipy.ndimage ref = scipy.ndimage.maximum_filter1d(S, max_size, axis=max_axis) import scipy.signal if zi is None: # Make sure zi matches dimension to input shape = tuple([1] * ref.ndim) zi = np.empty(shape) zi[:] = scipy.signal.lfilter_zi([b], [1, b - 1])[:] # Temporal integration S_smooth: np.ndarray zf: np.ndarray S_smooth, zf = scipy.signal.lfilter([b], [1, b - 1], ref, zi=zi, axis=axis) # Adaptive gain control # Working in log-space gives us some stability, and a slight speedup smooth = np.exp(-gain * (np.log(eps) + np.log1p(S_smooth / eps))) # Dynamic range compression S_out: np.ndarray if power == 0: S_out = np.log1p(S * smooth) elif bias == 0: S_out = np.exp(power * (np.log(S) + np.log(smooth))) else: S_out = (bias**power) * np.expm1(power * np.log1p(S * smooth / bias)) if return_zf: return S_out, zf else: return S_out def griffinlim( S: np.ndarray, *, n_iter: int = 32, hop_length: int | None = None, win_length: int | None = None, n_fft: int | None = None, window: _WindowSpec = "hann", center: bool = True, dtype: DTypeLike | None = None, length: int | None = None, pad_mode: _PadModeSTFT = "constant", momentum: float = 0.99, init: str | None = "random", rng: RNGLike | SeedLike | None = None, random_state: int | np.random.RandomState | np.random.Generator | Deprecated | None = Deprecated(), ) -> np.ndarray: """Approximate magnitude spectrogram inversion using the "fast" Griffin-Lim algorithm. Given a short-time Fourier transform magnitude matrix (``S``), the algorithm randomly initializes phase estimates, and then alternates forward- and inverse-STFT operations. [#]_ Note that this assumes reconstruction of a real-valued time-domain signal, and that ``S`` contains only the non-negative frequencies (as computed by `stft`). The "fast" GL method [#]_ uses a momentum parameter to accelerate convergence. .. [#] D. W. Griffin and J. S. Lim, "Signal estimation from modified short-time Fourier transform," IEEE Trans. ASSP, vol.32, no.2, pp.236--243, Apr. 1984. .. [#] Perraudin, N., Balazs, P., & Søndergaard, P. L. "A fast Griffin-Lim algorithm," IEEE Workshop on Applications of Signal Processing to Audio and Acoustics (pp. 1-4), Oct. 2013. Parameters ---------- S : np.ndarray [shape=(..., n_fft // 2 + 1, t), non-negative] An array of short-time Fourier transform magnitudes as produced by `stft`. n_iter : int > 0 The number of iterations to run hop_length : None or int > 0 The hop length of the STFT. If not provided, it will default to ``n_fft // 4`` win_length : None or int > 0 The window length of the STFT. By default, it will equal ``n_fft`` n_fft : None or int > 0 The number of samples per frame. By default, this will be inferred from the shape of ``S`` as an even number. However, if an odd frame length was used, you can explicitly set ``n_fft``. window : str, tuple, number, function, or np.ndarray [shape=(n_fft,)] A window specification as supported by `stft` or `istft` center : bool If ``True``, the STFT is assumed to use centered frames. If ``False``, the STFT is assumed to use left-aligned frames. dtype : np.dtype Real numeric type for the time-domain signal. Default is inferred to match the precision of the input spectrogram. length : None or int > 0 If provided, the output ``y`` is zero-padded or clipped to exactly ``length`` samples. pad_mode : str If ``center=True``, the padding mode to use at the edges of the signal. By default, STFT uses zero padding. momentum : number >= 0 The momentum parameter for fast Griffin-Lim. Setting this to 0 recovers the original Griffin-Lim method [1]_. Values near 1 can lead to faster convergence, but above 1 may not converge. init : None or 'random' [default] If 'random' (the default), then phase values are initialized randomly according to ``random_state``. This is recommended when the input ``S`` is a magnitude spectrogram with no initial phase estimates. If `None`, then the phase is initialized from ``S``. This is useful when an initial guess for phase can be provided, or when you want to resume Griffin-Lim from a previous output. rng : None, int, sequence of int, np.random.Generator, or np.random.RandomState Pseudorandom number generator state. When `rng` is None, a new `numpy.random.Generator` is created using entropy from the operating system. Types other than `numpy.random.Generator` are passed to `numpy.random.default_rng` to instantiate a ``Generator``. random_state : None, int, np.random.RandomState, or np.random.Generator .. warning:: This parameter is deprecated in 1.0.0 and will be removed in 1.2.0. If int, random_state is the seed used by the random number generator for phase initialization. If `np.random.RandomState` or `np.random.Generator` instance, the random number generator itself. If `None`, defaults to the `np.random.default_rng()` object. An exception is raised if both `rng` and `random_state` are provided. Returns ------- y : np.ndarray [shape=(..., n)] time-domain signal reconstructed from ``S`` See Also -------- stft istft magphase filters.get_window Examples -------- A basic STFT inverse example >>> y, sr = librosa.loadx('trumpet') >>> # Get the magnitude spectrogram >>> S = np.abs(librosa.stft(y)) >>> # Invert using Griffin-Lim >>> y_inv = librosa.griffinlim(S) >>> # Invert without estimating phase >>> y_istft = librosa.istft(S) Wave-plot the results >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=3, sharex=True, sharey=True) >>> librosa.display.waveshow(y, sr=sr, color='C0', ax=ax[0]) >>> ax[0].set(title='Original', xlabel=None) >>> ax[0].label_outer() >>> librosa.display.waveshow(y_inv, sr=sr, color='C1', ax=ax[1]) >>> ax[1].set(title='Griffin-Lim reconstruction', xlabel=None) >>> ax[1].label_outer() >>> librosa.display.waveshow(y_istft, sr=sr, color='C2', ax=ax[2]) >>> ax[2].set_title('Magnitude-only istft reconstruction') """ if not isinstance(random_state, Deprecated): if rng is not None: raise ParameterError( f"Both random_state={random_state!r} and rng={rng!r} were provided. " "Please use only the rng parameter." ) # Otherwise transfer the state object and throw a deprecation warning rng = rename_kw( old_name="random_state", old_value=random_state, new_name="rng", new_value=rng, version_deprecated="1.0.0", version_removed="1.2.0", ) # Coerce the various input types to a proper Generator # This branch is necessary until we bump to numpy 2.2 if not isinstance(rng, np.random.RandomState): rng = np.random.default_rng(rng) if momentum > 1: warnings.warn( f"Griffin-Lim with momentum={momentum} > 1 can be unstable. " "Proceed with caution!", stacklevel=2, ) elif momentum < 0: raise ParameterError(f"griffinlim() called with momentum={momentum} < 0") # Infer n_fft from the spectrogram shape if n_fft is None: n_fft = 2 * (S.shape[-2] - 1) # Infer the dtype from S angles = np.empty(S.shape, dtype=util.dtype_r2c(S.dtype)) eps = util.tiny(angles) if init == "random": # randomly initialize the phase angles[:] = util.phasor((2 * np.pi * rng.random(size=S.shape))) elif init is None: # Initialize an all ones complex matrix angles[:] = 1.0 else: raise ParameterError(f"init={init} must either None or 'random'") # Place-holders for temporary data and reconstructed buffer rebuilt = None tprev = None inverse = None # Absorb magnitudes into angles angles *= S for _ in range(n_iter): # Invert with our current estimate of the phases inverse = istft( angles, hop_length=hop_length, win_length=win_length, n_fft=n_fft, window=window, center=center, dtype=dtype, length=length, out=inverse, ) # Rebuild the spectrogram rebuilt = stft( inverse, n_fft=n_fft, hop_length=hop_length, win_length=win_length, window=window, center=center, pad_mode=pad_mode, out=rebuilt, ) # Update our phase estimates angles[:] = rebuilt if tprev is not None: angles -= (momentum / (1 + momentum)) * tprev angles /= np.abs(angles) + eps angles *= S # Store rebuilt, tprev = tprev, rebuilt # Return the final phase estimates return istft( angles, hop_length=hop_length, win_length=win_length, n_fft=n_fft, window=window, center=center, dtype=dtype, length=length, out=inverse, ) def _spectrogram( *, y: np.ndarray | None = None, S: np.ndarray | None = None, n_fft: int | None = 2048, hop_length: int | None = 512, power: float = 1, win_length: int | None = None, window: _WindowSpec = "hann", center: bool = True, pad_mode: _PadModeSTFT = "constant", ) -> tuple[np.ndarray, int]: """Retrieve a magnitude spectrogram. This is primarily used in feature extraction functions that can operate on either audio time-series or spectrogram input. Parameters ---------- y : None or np.ndarray If provided, an audio time series S : None or np.ndarray Spectrogram input, optional n_fft : int > 0 length of the FFT frame hop_length : int > 0 STFT hop length power : float > 0 Exponent for the magnitude spectrogram, e.g., 1 for energy, 2 for power, etc. win_length : int <= n_fft [scalar] Each frame of audio is windowed by ``window``. The window will be of length ``win_length`` and then padded with zeros to match ``n_fft``. If unspecified, defaults to ``win_length = n_fft``. window : str, tuple, number, function, or np.ndarray [shape=(n_fft,)] - a window specification (str, tuple, or number); see `scipy.signal.get_window` - a window function, such as `scipy.signal.windows.hann` - a vector or array of length ``n_fft`` .. see also:: `filters.get_window` center : bool - If ``True``, the signal ``y`` is padded so that frame ``t`` is centered at ``y[t * hop_length]``. - If ``False``, then frame ``t`` begins at ``y[t * hop_length]`` pad_mode : str If ``center=True``, the padding mode to use at the edges of the signal. By default, STFT uses zero padding. Returns ------- S_out : np.ndarray [dtype=np.float] - If ``S`` is provided as input, then ``S_out == S`` - Else, ``S_out = |stft(y, ...)|**power`` n_fft : int > 0 - If ``S`` is provided, then ``n_fft`` is inferred from ``S`` - Else, copied from input """ if S is not None: # Infer n_fft from spectrogram shape, but only if it mismatches if n_fft is None or n_fft // 2 + 1 != S.shape[-2]: n_fft = 2 * (S.shape[-2] - 1) else: # Otherwise, compute a magnitude spectrogram from input if n_fft is None: raise ParameterError(f"Unable to compute spectrogram with n_fft={n_fft}") if y is None: raise ParameterError( "Input signal must be provided to compute a spectrogram" ) S = ( np.abs( stft( y, n_fft=n_fft, hop_length=hop_length, win_length=win_length, center=center, window=window, pad_mode=pad_mode, ) ) ** power ) return S, n_fft ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2102268 librosa-1.0.0/librosa/decompose.py0000644000175100017510000004753115236640653016620 0ustar00runnerrunner#!/usr/bin/env python # -*- coding: utf-8 -*- """ librosa.decompose provides functions to decompose a feature matrix or spectrogram into components. This includes harmonic-percussive source separation (HPSS), nearest-neighbor filtering, and general matrix decomposition (e.g. NMF). """ from __future__ import annotations from typing import TYPE_CHECKING import numpy as np import scipy.sparse from . import core, segment, util from ._cache import cache from .util.exceptions import ParameterError if TYPE_CHECKING: from typing import Any, Callable from ._typing import _FloatLike_co, _IntLike_co, _SparseArray __all__ = ["decompose", "hpss", "nn_filter"] def decompose( S: np.ndarray, *, n_components: int | None = None, transformer: object | None = None, sort: bool = False, fit: bool = True, **kwargs: Any, ) -> tuple[np.ndarray, np.ndarray]: """Decompose a feature matrix. Given a spectrogram ``S``, produce a decomposition into ``components`` and ``activations`` such that ``S ~= components.dot(activations)``. By default, this is done with with non-negative matrix factorization (NMF), but any `sklearn.decomposition`-type object will work. Parameters ---------- S : np.ndarray [shape=(..., n_features, n_samples), dtype=float] The input feature matrix (e.g., magnitude spectrogram) If the input has multiple channels (leading dimensions), they will be automatically flattened prior to decomposition. If the input is multi-channel, channels and features are automatically flattened into a single axis before the decomposition. For example, a stereo input `S` with shape `(2, n_features, n_samples)` is automatically reshaped to `(2 * n_features, n_samples)`. n_components : int > 0 [scalar] or None number of desired components if None, then ``n_features`` components are used transformer : None or object If None, use `sklearn.decomposition.NMF` Otherwise, any object with a similar interface to NMF should work. ``transformer`` must follow the scikit-learn convention, where input data is ``(n_samples, n_features)``. `transformer.fit_transform()` will be run on ``S.T`` (not ``S``), the return value of which is stored (transposed) as ``activations`` The components will be retrieved as ``transformer.components_.T``:: S ~= np.dot(activations, transformer.components_).T or equivalently:: S ~= np.dot(transformer.components_.T, activations.T) sort : bool If ``True``, components are sorted by ascending peak frequency. .. note:: If used with ``transformer``, sorting is applied to copies of the decomposition parameters, and not to ``transformer`` internal parameters. .. warning:: If the input array has more than two dimensions (e.g., if it's a multi-channel spectrogram), then axis sorting is not supported and a `ParameterError` exception is raised. fit : bool If `True`, components are estimated from the input ``S``. If `False`, components are assumed to be pre-computed and stored in ``transformer``, and are not changed. **kwargs : Additional keyword arguments to the default transformer `sklearn.decomposition.NMF` Returns ------- components: np.ndarray [shape=(..., n_features, n_components)] matrix of components (basis elements). activations: np.ndarray [shape=(n_components, n_samples)] transformed matrix/activation matrix Raises ------ ParameterError if ``fit`` is False and no ``transformer`` object is provided. if the input array is multi-channel and ``sort=True`` is specified. See Also -------- sklearn.decomposition : SciKit-Learn matrix decomposition modules Examples -------- Decompose a magnitude spectrogram into 16 components with NMF >>> y, sr = librosa.loadx('pistachio', duration=5) >>> S = np.abs(librosa.stft(y)) >>> comps, acts = librosa.decompose.decompose(S, n_components=16) Sort components by ascending peak frequency >>> comps, acts = librosa.decompose.decompose(S, n_components=16, ... sort=True) Or with sparse dictionary learning >>> import sklearn.decomposition >>> T = sklearn.decomposition.MiniBatchDictionaryLearning(n_components=16) >>> scomps, sacts = librosa.decompose.decompose(S, transformer=T, sort=True) >>> import matplotlib.pyplot as plt >>> layout = [list(".AAAA"), list("BCCCC"), list(".DDDD")] >>> fig, ax = plt.subplot_mosaic(layout, constrained_layout=True) >>> librosa.display.specshow(S, vscale='dBFS', ... y_axis='log', x_axis='time', ax=ax['A']) >>> ax['A'].set(title='Input spectrogram') >>> ax['A'].label_outer() >>> librosa.display.specshow(comps, vscale='dBFS', ... y_axis='log', ax=ax['B']) >>> ax['B'].set(title='Components') >>> ax['B'].label_outer() >>> ax['B'].sharey(ax['A']) >>> librosa.display.specshow(acts, x_axis='time', ax=ax['C'], cmap='gray_r') >>> ax['C'].set(ylabel='Components', title='Activations') >>> ax['C'].sharex(ax['A']) >>> ax['C'].label_outer() >>> S_approx = comps.dot(acts) >>> img = librosa.display.specshow(S_approx, vscale='dBFS', ... y_axis='log', x_axis='time', ax=ax['D']) >>> ax['D'].set(title='Reconstructed spectrogram') >>> ax['D'].sharex(ax['A']) >>> ax['D'].sharey(ax['A']) >>> ax['D'].label_outer() >>> librosa.display.colorbar_db(img, ax=list(ax.values())) """ # Do a swapaxes and unroll orig_shape = list(S.shape) if S.ndim > 2 and sort: raise ParameterError( "Parameter sort=True is unsupported for input with more than two dimensions" ) # Transpose S and unroll feature dimensions # Use order='F' here to preserve the temporal ordering S = S.T.reshape((S.shape[-1], -1), order="F") if n_components is None: n_components = S.shape[-1] if transformer is None: if fit is False: raise ParameterError("fit must be True if transformer is None") import sklearn.decomposition transformer = sklearn.decomposition.NMF(n_components=n_components, **kwargs) # Suppressing type errors here because we don't want to overly restrict # the transformer object type activations: np.ndarray if fit: activations = transformer.fit_transform(S).T # type: ignore else: activations = transformer.transform(S).T # type: ignore components: np.ndarray = transformer.components_ # type: ignore component_shape = [*orig_shape[:-1], -1] # use order='F' here to preserve component ordering components = components.reshape(component_shape[::-1], order="F").T if sort: components, idx = util.axis_sort(components, index=True) activations = activations[idx] return components, activations @cache(level=30) def hpss( S: np.ndarray, *, kernel_size: _IntLike_co | tuple[_IntLike_co, _IntLike_co] | list[_IntLike_co] = 31, power: float = 2.0, mask: bool = False, margin: _FloatLike_co | tuple[_FloatLike_co, _FloatLike_co] | list[_FloatLike_co] = 1.0, ) -> tuple[np.ndarray, np.ndarray]: """Median-filtering harmonic percussive source separation (HPSS). If ``margin = 1.0``, decomposes an input spectrogram ``S = H + P`` where ``H`` contains the harmonic components, and ``P`` contains the percussive components. If ``margin > 1.0``, decomposes an input spectrogram ``S = H + P + R`` where ``R`` contains residual components not included in ``H`` or ``P``. This implementation is based upon the algorithm described by [#]_ and [#]_. .. [#] Fitzgerald, Derry. "Harmonic/percussive separation using median filtering." 13th International Conference on Digital Audio Effects (DAFX10), Graz, Austria, 2010. .. [#] Driedger, Müller, Disch. "Extending harmonic-percussive separation of audio." 15th International Society for Music Information Retrieval Conference (ISMIR 2014), Taipei, Taiwan, 2014. Parameters ---------- S : np.ndarray [shape=(..., d, n)] input spectrogram. May be real (magnitude) or complex. Multi-channel is supported. kernel_size : int or tuple (kernel_harmonic, kernel_percussive) kernel size(s) for the median filters. - If scalar, the same size is used for both harmonic and percussive. - If tuple, the first value specifies the width of the harmonic filter, and the second value specifies the width of the percussive filter. power : float > 0 [scalar] Exponent for the Wiener filter when constructing soft mask matrices. mask : bool Return the masking matrices instead of components. Masking matrices contain non-negative real values that can be used to measure the assignment of energy from ``S`` into harmonic or percussive components. Components can be recovered by multiplying ``S * mask_H`` or ``S * mask_P``. margin : float or tuple (margin_harmonic, margin_percussive) margin size(s) for the masks (as described in [2]_) - If scalar, the same size is used for both harmonic and percussive. - If tuple, the first value specifies the margin of the harmonic mask, and the second value specifies the margin of the percussive mask. Returns ------- harmonic : np.ndarray [shape=(..., d, n)] harmonic component (or mask) percussive : np.ndarray [shape=(..., d, n)] percussive component (or mask) See Also -------- librosa.util.softmask Notes ----- This function caches at level 30. Examples -------- Separate into harmonic and percussive >>> y, sr = librosa.loadx('choice', duration=5) >>> D = librosa.stft(y) >>> H, P = librosa.decompose.hpss(D) >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=3, sharex=True, sharey=True) >>> img = librosa.display.specshow(D, vscale='dBFS', ... y_axis='log', x_axis='time', ax=ax[0]) >>> ax[0].set(title='Full power spectrogram') >>> ax[0].label_outer() >>> ref = np.max(np.abs(D)) # reference for dBFS scaling comes from full mix >>> librosa.display.specshow(H, vscale=f'dB[{ref}]', ... y_axis='log', x_axis='time', ax=ax[1]) >>> ax[1].set(title='Harmonic power spectrogram') >>> ax[1].label_outer() >>> librosa.display.specshow(P, vscale=f'dB[{ref}]', ... y_axis='log', x_axis='time', ax=ax[2]) >>> ax[2].set(title='Percussive power spectrogram') >>> librosa.display.colorbar_db(img, ax=ax) Or with a narrower horizontal filter >>> H, P = librosa.decompose.hpss(D, kernel_size=(13, 31)) Just get harmonic/percussive masks, not the spectra >>> mask_H, mask_P = librosa.decompose.hpss(D, mask=True) >>> mask_H array([[1.853e-03, 1.701e-04, ..., 9.922e-01, 1.000e+00], [2.316e-03, 2.127e-04, ..., 9.989e-01, 1.000e+00], ..., [8.195e-05, 6.939e-05, ..., 3.105e-04, 4.231e-04], [3.159e-05, 4.156e-05, ..., 6.216e-04, 6.188e-04]], dtype=float32) >>> mask_P array([[9.981e-01, 9.998e-01, ..., 7.759e-03, 3.201e-05], [9.977e-01, 9.998e-01, ..., 1.122e-03, 4.451e-06], ..., [9.999e-01, 9.999e-01, ..., 9.997e-01, 9.996e-01], [1.000e+00, 1.000e+00, ..., 9.994e-01, 9.994e-01]], dtype=float32) Separate into harmonic/percussive/residual components by using a margin > 1.0 >>> H, P = librosa.decompose.hpss(D, margin=3.0) >>> R = D - (H+P) >>> y_harm = librosa.istft(H) >>> y_perc = librosa.istft(P) >>> y_resi = librosa.istft(R) Get a more isolated percussive component by widening its margin >>> H, P = librosa.decompose.hpss(D, margin=(1.0,5.0)) """ phase: float | np.ndarray if np.iscomplexobj(S): S, phase = core.magphase(S) else: phase = 1 if isinstance(kernel_size, (tuple, list)): win_harm = kernel_size[0] win_perc = kernel_size[1] else: win_harm = kernel_size win_perc = kernel_size if isinstance(margin, (tuple, list)): margin_harm = margin[0] margin_perc = margin[1] else: margin_harm = margin margin_perc = margin # margin minimum is 1.0 if margin_harm < 1 or margin_perc < 1: raise ParameterError( "Margins must be >= 1.0. " "A typical range is between 1 and 10." ) # shape for kernels harm_shape = [1] * S.ndim harm_shape[-1] = int(win_harm) perc_shape = [1] * S.ndim perc_shape[-2] = int(win_perc) # Compute median filters. Pre-allocation here preserves memory layout. from scipy.ndimage import median_filter harm = np.empty_like(S) harm[:] = median_filter(S, size=harm_shape, mode="reflect") perc = np.empty_like(S) perc[:] = median_filter(S, size=perc_shape, mode="reflect") split_zeros = margin_harm == 1 and margin_perc == 1 mask_harm = util.softmask( harm, perc * margin_harm, power=power, split_zeros=split_zeros ) mask_perc = util.softmask( perc, harm * margin_perc, power=power, split_zeros=split_zeros ) if mask: return mask_harm, mask_perc return ((S * mask_harm) * phase, (S * mask_perc) * phase) @cache(level=30) def nn_filter( S: np.ndarray, *, rec: np.ndarray | _SparseArray | None = None, aggregate: Callable | None = None, axis: int = -1, **kwargs: Any, ) -> np.ndarray: """Filter by nearest-neighbor aggregation. Each data point (e.g, spectrogram column) is replaced by aggregating its nearest neighbors in feature space. This can be useful for de-noising a spectrogram or feature matrix. The non-local means method [#]_ can be recovered by providing a weighted recurrence matrix as input and specifying ``aggregate=np.average``. Similarly, setting ``aggregate=np.median`` produces sparse de-noising as in REPET-SIM [#]_. .. [#] Buades, A., Coll, B., & Morel, J. M. (2005, June). A non-local algorithm for image denoising. In Computer Vision and Pattern Recognition, 2005. CVPR 2005. IEEE Computer Society Conference on (Vol. 2, pp. 60-65). IEEE. .. [#] Rafii, Z., & Pardo, B. (2012, October). "Music/Voice Separation Using the Similarity Matrix." International Society for Music Information Retrieval Conference, 2012. Parameters ---------- S : np.ndarray The input data (spectrogram) to filter. Multi-channel is supported. rec : (optional) scipy.sparse array or np.ndarray Optionally, a pre-computed nearest-neighbor array as provided by `librosa.segment.recurrence_matrix` aggregate : function aggregation function (default: `np.mean`) If ``aggregate=np.average``, then a weighted average is computed according to the (per-row) weights in ``rec``. For all other aggregation functions, all neighbors are treated equally. axis : int The axis along which to filter (by default, columns) **kwargs Additional keyword arguments provided to `librosa.segment.recurrence_matrix` if ``rec`` is not provided Returns ------- S_filtered : np.ndarray The filtered data, with shape equivalent to the input ``S``. Raises ------ ParameterError if ``rec`` is provided and its shape is incompatible with ``S``. See Also -------- decompose hpss librosa.segment.recurrence_matrix Notes ----- This function caches at level 30. Examples -------- De-noise a chromagram by non-local median filtering. By default this would use euclidean distance to select neighbors, but this can be overridden directly by setting the ``metric`` parameter. >>> y, sr = librosa.loadx('brahms', offset=30, duration=10) >>> chroma = librosa.feature.chroma_cqt(y=y, sr=sr) >>> chroma_med = librosa.decompose.nn_filter(chroma, ... aggregate=np.median, ... metric='cosine') To use non-local means, provide an affinity matrix and ``aggregate=np.average``. >>> rec = librosa.segment.recurrence_matrix(chroma, mode='affinity', ... metric='cosine', sparse=True) >>> chroma_nlm = librosa.decompose.nn_filter(chroma, rec=rec, ... aggregate=np.average) >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=5, sharex=True, sharey=True, figsize=(10, 10)) >>> librosa.display.specshow(chroma, y_axis='chroma', x_axis='time', ax=ax[0]) >>> ax[0].set(title='Unfiltered') >>> ax[0].label_outer() >>> librosa.display.specshow(chroma_med, y_axis='chroma', x_axis='time', ax=ax[1]) >>> ax[1].set(title='Median-filtered') >>> ax[1].label_outer() >>> imgc = librosa.display.specshow(chroma_nlm, y_axis='chroma', x_axis='time', ax=ax[2]) >>> ax[2].set(title='Non-local means') >>> ax[2].label_outer() >>> imgr1 = librosa.display.specshow(chroma - chroma_med, ... y_axis='chroma', x_axis='time', ax=ax[3]) >>> ax[3].set(title='Original - median') >>> ax[3].label_outer() >>> imgr2 = librosa.display.specshow(chroma - chroma_nlm, ... y_axis='chroma', x_axis='time', ax=ax[4]) >>> ax[4].label_outer() >>> ax[4].set(title='Original - NLM') >>> fig.colorbar(imgc, ax=ax[:3]) >>> fig.colorbar(imgr1, ax=[ax[3]]) >>> fig.colorbar(imgr2, ax=[ax[4]]) """ if aggregate is None: aggregate = np.mean if rec is None: kwargs = dict(kwargs) kwargs["sparse"] = True rec_s = segment.recurrence_matrix(S, axis=axis, **kwargs) elif not scipy.sparse.issparse(rec): rec_s = scipy.sparse.csc_array(rec) else: # Normalize any sparse input (matrix/array, any format) to csc_array rec_s = scipy.sparse.csc_array(rec) if rec_s.shape[0] != S.shape[axis] or rec_s.shape[0] != rec_s.shape[1]: raise ParameterError( "Invalid self-similarity matrix shape " f"rec.shape={rec_s.shape} for S.shape={S.shape}" ) return __nn_filter_helper( rec_s.data, rec_s.indices, rec_s.indptr, S.swapaxes(0, axis), aggregate ).swapaxes(0, axis) def __nn_filter_helper( R_data, R_indices, R_ptr, S: np.ndarray, aggregate: Callable ) -> np.ndarray: """Nearest-neighbor filter helper function. This is an internal function, not for use outside of the decompose module. It applies the nearest-neighbor filter to S, assuming that the first index corresponds to observations. Parameters ---------- R_data, R_indices, R_ptr : np.ndarrays The ``data``, ``indices``, and ``indptr`` of a scipy.sparse matrix S : np.ndarray The observation data to filter aggregate : callable The aggregation operator Returns ------- S_out : np.ndarray like S The filtered data array """ s_out = np.empty_like(S) for i in range(len(R_ptr) - 1): # Get the non-zeros out of the recurrence matrix targets = R_indices[R_ptr[i] : R_ptr[i + 1]] if not len(targets): s_out[i] = S[i] continue neighbors = np.take(S, targets, axis=0) if aggregate is np.average: weights = R_data[R_ptr[i] : R_ptr[i + 1]] s_out[i] = aggregate(neighbors, axis=0, weights=weights) else: s_out[i] = aggregate(neighbors, axis=0) return s_out ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2112267 librosa-1.0.0/librosa/display.py0000644000175100017510000043211015236640653016276 0ustar00runnerrunner#!/usr/bin/env python # -*- coding: utf-8 -*- """Visualizing signals and spectral data. Librosa display functionality is built on top of matplotlib. The display module provides a collection of convenience functions that make construction of common visualizations easier, and also provides a set of custom tick formatters for musical axes (time, frequency, pitch, etc.). """ from __future__ import annotations import colorsys import copy import re import warnings import weakref from fractions import Fraction from itertools import cycle, product from typing import TYPE_CHECKING, cast import matplotlib.axes as mplaxes import matplotlib.cm as cm import matplotlib.collections as mcollections import matplotlib.colors as colors import matplotlib.lines as mlines import matplotlib.patches as mpatches import matplotlib.patheffects as mpe import matplotlib.pyplot as plt import matplotlib.ticker as mplticker import matplotlib.transforms as mtransforms import numpy as np from matplotlib import colormaps as mcm from matplotlib.legend import Legend from matplotlib.legend_handler import HandlerBase, HandlerLine2D, HandlerPatch from . import core, util from .util.decorators import moved from .util.exceptions import ParameterError if TYPE_CHECKING: from typing import Any, Callable, Collection, Literal, Sequence import cycler import matplotlib import matplotlib.axes import matplotlib.figure import numpy.typing as npt import scipy.interpolate from matplotlib.artist import Artist from matplotlib.collections import PolyCollection, QuadMesh from matplotlib.colors import Colormap from matplotlib.lines import Line2D from matplotlib.markers import MarkerStyle from matplotlib.path import Path as MplPath from matplotlib.typing import ColorType from ._typing import ArrayLike, _Array1D, _FloatLike_co __all__ = [ "specshow", "waveshow", "wavebars", "wavef0", "multiplot", "highlight", "infer_cmap", "colorbar_db", "colorbar_phase", "legend_for_axes", "TimeFormatter", "NoteFormatter", "FJSFormatter", "LogHzFormatter", "ChromaFormatter", "ChromaSvaraFormatter", "ChromaFJSFormatter", "TonnetzFormatter", "AdaptiveWaveplot", "Transformf0", ] # mypy: disable-error-code="attr-defined" # Keeps adaptors alive as long as their Axes exists, preventing GC _WAVESHOW_ADAPTORS: weakref.WeakKeyDictionary[mplaxes.Axes, set["AdaptiveWaveplot"]] = ( weakref.WeakKeyDictionary() ) # Nominal center frequencies for oct3 bands __OCT3_FREQUENCIES = np.array( [ 31.5, 40, 50, 63, 80, 100, 125, 160, 200, 250, 315, 400, 500, 630, 800, 1000, 1250, 1600, 2000, 2500, 3150, 4000, 5000, 6300, 8000, 10000, 12500, 16000, 20000, 25000, # --- ultrasonic up to 800KHz 31500, 40000, 50000, 63000, 80000, 100000, 125000, 160000, 200000, 250000, 315000, 400000, 500000, 630000, 800000, ] ) class TimeFormatter(mplticker.Formatter): """A tick formatter for time axes. Automatically switches between seconds, minutes:seconds, or hours:minutes:seconds. Parameters ---------- lag : bool If ``True``, then the time axis is interpreted in lag coordinates. Anything past the midpoint will be converted to negative time. unit : str or None Abbreviation of the string representation for axis labels and ticks. List of supported units: * `"h"`: hour-based format (`H:MM:SS`) * `"m"`: minute-based format (`M:SS`) * `"s"`: second-based format (`S.sss` in scientific notation) * `"ms"`: millisecond-based format (`s.µµµ` in scientific notation) * `None`: adaptive to the duration of the underlying time range: similar to `"h"` above 3600 seconds; to `"m"` between 60 and 3600 seconds; to `"s"` between 1 and 60 seconds; and to `"ms"` below 1 second. See Also -------- matplotlib.ticker.Formatter Examples -------- For normal time >>> import matplotlib.pyplot as plt >>> times = np.arange(30) >>> values = np.random.randn(len(times)) >>> fig, ax = plt.subplots() >>> ax.plot(times, values) >>> ax.xaxis.set_major_formatter(librosa.display.TimeFormatter()) >>> ax.set(xlabel='Time') >>> plt.show() Manually set the physical time unit of the x-axis to milliseconds >>> times = np.arange(100) >>> values = np.random.randn(len(times)) >>> fig, ax = plt.subplots() >>> ax.plot(times, values) >>> ax.xaxis.set_major_formatter(librosa.display.TimeFormatter(unit='ms')) >>> ax.set(xlabel='Time (ms)') >>> plt.show() For lag plots >>> times = np.arange(60) >>> values = np.random.randn(len(times)) >>> fig, ax = plt.subplots() >>> ax.plot(times, values) >>> ax.xaxis.set_major_formatter(librosa.display.TimeFormatter(lag=True)) >>> ax.set(xlabel='Lag') >>> plt.show() """ unit: str | None lag: bool def __init__(self, lag: bool = False, unit: str | None = None): if unit not in ["h", "m", "s", "ms", None]: raise ParameterError(f"Unknown time unit: {unit}") super().__init__() self.unit = unit self.lag = lag def __call__(self, x: float, pos: int | None = None) -> str: """Return the time format as pos""" assert self.axis is not None _, dmax = self.axis.get_data_interval() vmin, vmax = self.axis.get_view_interval() # In lag-time axes, anything greater than dmax / 2 is negative time if self.lag and x >= dmax * 0.5: # In lag mode, don't tick past the limits of the data if x > dmax: return "" value = np.abs(x - dmax) # Do we need to tweak vmin/vmax here? sign = "-" else: value = x sign = "" if self.unit == "h" or ((self.unit is None) and (vmax - vmin > 3600)): s = "{:d}:{:02d}:{:02d}".format( int(value / 3600.0), int(np.mod(value / 60.0, 60)), int(np.mod(value, 60)), ) elif self.unit == "m" or ((self.unit is None) and (vmax - vmin > 60)): s = "{:d}:{:02d}".format(int(value / 60.0), int(np.mod(value, 60))) elif self.unit == "s": s = f"{value:.3g}" elif self.unit is None and (vmax - vmin >= 1): s = f"{value:.2g}" elif self.unit == "ms": s = "{:.3g}".format(value * 1000) elif self.unit is None and (vmax - vmin < 1): s = f"{value:.3f}" return f"{sign:s}{s:s}" class AdaptiveFormatterBase(mplticker.Formatter): """Base formatter handling 2-octave span suppression. Subclasses must implement `_format_tick`. Parameters ---------- major : bool If ``True``, ticks are always labeled. If ``False``, ticks are only labeled if the span is less than 2 octaves. """ major: bool vmin: float | None vmax: float | None def __init__(self, major: bool = True): super().__init__() self.major = major self.vmin = None self.vmax = None def __call__(self, x: float, pos: int | None = None) -> str: """Apply the bounds check, then delegate to subclass formatting.""" if x <= 0: return "" assert self.axis is not None vmin, vmax = self.axis.get_view_interval() # Handle inverted axes self.vmin, self.vmax = (vmin, vmax) if vmin <= vmax else (vmax, vmin) if not self.major and self.vmax > 4 * max(1, self.vmin): return "" return self._format_tick(x, pos) def _format_tick(self, x: float, pos: int | None = None) -> str: raise NotImplementedError class NoteFormatter(AdaptiveFormatterBase): """Ticker formatter for Notes Parameters ---------- octave : bool If ``True``, display the octave number along with the note name. Otherwise, only show the note name (and cent deviation) major : bool If ``True``, ticks are always labeled. If ``False``, ticks are only labeled if the span is less than 2 octaves key : str Key for determining pitch spelling. unicode : bool If ``True``, use unicode symbols for accidentals. If ``False``, use ASCII symbols for accidentals. See Also -------- LogHzFormatter matplotlib.ticker.Formatter """ octave: bool major: bool key: str unicode: bool def __init__( self, octave: bool = True, major: bool = True, key: str = "C:maj", unicode: bool = True, ): super().__init__(major=major) self.octave = octave self.key = key self.unicode = unicode def _format_tick(self, x: float, pos: int | None = None) -> str: """Apply the formatter to position""" # Only use cent precision if our vspan is less than an octave assert self.vmax is not None and self.vmin is not None cents = self.vmax <= 2 * max(1, self.vmin) return core.hz_to_note( x, octave=self.octave, cents=cents, key=self.key, unicode=self.unicode ) class SvaraFormatter(AdaptiveFormatterBase): """Ticker formatter for Svara Parameters ---------- Sa : number > 0 Frequency (in Hz) of Sa octave : bool If ``True``, display the octave number along with the note name. Otherwise, only show the note name (and cent deviation) major : bool If ``True``, ticks are always labeled. If ``False``, ticks are only labeled if the span is less than 2 octaves abbr : bool If ``True``, use abbreviated svara names. If ``False``, use full svara names. mela : str or int For Carnatic svara, the index or name of the melakarta raga in question To use Hindustani svara, set ``mela=None`` unicode : bool If ``True``, use unicode symbols for accidentals. If ``False``, use ASCII symbols for accidentals. See Also -------- NoteFormatter matplotlib.ticker.Formatter librosa.hz_to_svara_c librosa.hz_to_svara_h """ def __init__( self, Sa: float, octave: bool = True, major: bool = True, abbr: bool = False, mela: str | int | None = None, unicode: bool = True, ): if Sa is None: raise ParameterError( "Sa frequency is required for svara display formatting" ) super().__init__(major=major) self.Sa = Sa self.octave = octave self.abbr = abbr self.mela = mela self.unicode = unicode def _format_tick(self, x: float, pos: int | None = None) -> str: if self.mela is None: return core.hz_to_svara_h( x, Sa=self.Sa, octave=self.octave, abbr=self.abbr, unicode=self.unicode ) else: return core.hz_to_svara_c( x, Sa=self.Sa, mela=self.mela, octave=self.octave, abbr=self.abbr, unicode=self.unicode, ) class FJSFormatter(AdaptiveFormatterBase): """Ticker formatter for Functional Just System (FJS) notation Parameters ---------- fmin : float The unison frequency for this axis n_bins : int > 0 The number of frequency bins. bins_per_octave : int > 0 The number of bins per octave. intervals : str or array of float in [1, 2) The interval specification for the frequency axis. See `core.interval_frequencies` for supported values. major : bool If ``True``, ticks are always labeled. If ``False``, ticks are only labeled if the span is less than 2 octaves unison : str The unison note name. If not provided, it will be inferred from fmin. unicode : bool If ``True``, use unicode symbols for accidentals. If ``False``, use ASCII symbols for accidentals. See Also -------- NoteFormatter hz_to_fjs matplotlib.ticker.Formatter """ fmin: float unison: str | None unicode: bool intervals: str | Collection[float] n_bins: int bins_per_octave: int frequencies_: np.ndarray[tuple[int], np.dtype[np.float64]] def __init__( self, *, fmin: float, n_bins: int, bins_per_octave: int, intervals: str | Collection[float], major: bool = True, unison: str | None = None, unicode: bool = True, ): super().__init__(major=major) self.fmin = fmin self.unison = unison self.unicode = unicode self.intervals = intervals self.n_bins = n_bins self.bins_per_octave = bins_per_octave self.frequencies_ = core.interval_frequencies( n_bins, fmin=fmin, intervals=intervals, bins_per_octave=bins_per_octave ) def _format_tick(self, x: float, pos: int | None = None) -> str: """Apply the formatter to position""" # Map the given frequency to the nearest JI interval idx = util.match_events(np.atleast_1d(x), self.frequencies_)[0] label: str = core.hz_to_fjs( self.frequencies_[idx], fmin=self.fmin, unison=self.unison, unicode=self.unicode, ) return label class LogHzFormatter(AdaptiveFormatterBase): """Ticker formatter for logarithmic frequency Parameters ---------- major : bool If ``True``, ticks are always labeled. If ``False``, ticks are only labeled if the span is less than 2 octaves See Also -------- NoteFormatter matplotlib.ticker.Formatter """ def __init__(self, major: bool = True): super().__init__(major=major) def _format_tick(self, x: float, pos: int | None = None) -> str: """Apply the formatter to position""" return f"{x:g}" class AdaptiveEngFormatter(AdaptiveFormatterBase): """Engineering formatter that limits tick labels to a 2-octave span. Parameters ---------- major : bool If ``True``, ticks are always labeled. If ``False``, ticks are only labeled if the span is less than 2 octaves **kwargs : keyword arguments Additional keyword arguments are passed to `matplotlib.ticker.EngFormatter`. """ def __init__(self, major: bool = True, **kwargs): super().__init__(major=major) self._formatter = mplticker.EngFormatter(**kwargs) def _format_tick(self, x: float, pos: int | None = None) -> str: # Delegate string conversion to the wrapped matplotlib formatter return self._formatter(x, pos) class ChromaFormatter(mplticker.Formatter): """A formatter for chroma axes Parameters ---------- key : str The key in which to display pitch class labels. See `core.midi_to_note` for supported values. unicode : bool If ``True``, use unicode symbols for accidentals. If ``False``, use ASCII symbols for accidentals. See Also -------- matplotlib.ticker.Formatter """ key: str unicode: bool def __init__(self, key: str = "C:maj", unicode: bool = True): super().__init__() self.key = key self.unicode = unicode def __call__(self, x: float, pos: int | None = None) -> str: """Format for chroma positions""" return core.midi_to_note( int(x), octave=False, cents=False, key=self.key, unicode=self.unicode ) class ChromaSvaraFormatter(mplticker.Formatter): """A formatter for chroma axes with svara instead of notes. If mela is given, Carnatic svara names will be used. Otherwise, Hindustani svara names will be used. If `Sa` is not given, it will default to 0 (equivalent to `C`). Parameters ---------- Sa : float or None The MIDI note number corresponding to Sa. If ``None``, defaults to 0 (C). mela : str, int, or None For Carnatic svara, the index or name of the melakarta raga. If ``None``, Hindustani svara names are used. abbr : bool If ``True``, use abbreviated svara names. If ``False``, use full svara names. unicode : bool If ``True``, use unicode symbols for accidentals. If ``False``, use ASCII symbols for accidentals. See Also -------- ChromaFormatter """ Sa: float mela: int | str | None abbr: bool unicode: bool def __init__( self, Sa: float | None = None, mela: int | str | None = None, abbr: bool = True, unicode: bool = True, ): super().__init__() if Sa is None: Sa = 0 self.Sa = Sa self.mela = mela self.abbr = abbr self.unicode = unicode def __call__(self, x: float, pos: int | None = None) -> str: """Format for chroma positions""" if self.mela is not None: return core.midi_to_svara_c( int(x), Sa=self.Sa, mela=self.mela, octave=False, abbr=self.abbr, unicode=self.unicode, ) else: return core.midi_to_svara_h( int(x), Sa=self.Sa, octave=False, abbr=self.abbr, unicode=self.unicode ) class ChromaFJSFormatter(mplticker.Formatter): """A formatter for chroma axes with functional just notation Parameters ---------- intervals : str or array of float in [1, 2) The interval specification for the chroma axis. See `core.interval_frequencies` for supported values. unison : str The unison (tonic) note name. unicode : bool If ``True``, use unicode symbols for accidentals. If ``False``, use ASCII symbols for accidentals. bins_per_octave : int or None The number of bins per octave. If ``None``, inferred from ``intervals``. See Also -------- matplotlib.ticker.Formatter """ unison: str unicode: bool intervals: str | Collection[float] bins_per_octave: int intervals_: np.ndarray[tuple[int], np.dtype[np.float64]] def __init__( self, *, intervals: str | Collection[float], unison: str = "C", unicode: bool = True, bins_per_octave: int | None = None, ): super().__init__() self.unison = unison self.unicode = unicode self.intervals = intervals try: if not isinstance(intervals, str): bins_per_octave = len(intervals) if not isinstance(bins_per_octave, int): raise ParameterError( f"bins_per_octave={bins_per_octave} must be integer-valued" ) self.bins_per_octave = bins_per_octave # Construct the explicit interval set self.intervals_ = core.interval_frequencies( self.bins_per_octave, fmin=1, intervals=intervals, bins_per_octave=self.bins_per_octave, ) except TypeError as exc: raise ParameterError( f"intervals={intervals} must be of type str or a collection of numbers between 1 and 2" ) from exc def __call__(self, x: float, pos: int | None = None) -> str: """Format for chroma positions""" lab: str = core.interval_to_fjs( self.intervals_[int(x) % self.bins_per_octave], unison=self.unison, unicode=self.unicode, ) return lab class TonnetzFormatter(mplticker.Formatter): """A formatter for tonnetz axes See Also -------- matplotlib.ticker.Formatter """ def __call__(self, x: float, pos: int | None = None) -> str: """Format for tonnetz positions""" return [r"5$_y$", r"5$_x$", r"m3$_y$", r"m3$_x$", r"M3$_y$", r"M3$_x$"][int(x)] class AdaptiveWaveplot: """A helper class for managing adaptive wave visualizations. This object is used to dynamically switch between sample-based and envelope-based visualizations of waveforms. When the display is zoomed in such that no more than `max_samples` would be visible, the sample-based display is used. When displaying the raw samples would require more than `max_samples`, an envelope-based plot is used instead. You should never need to instantiate this object directly, as it is constructed automatically by `waveshow`. Parameters ---------- times : np.ndarray An array containing the time index (in seconds) for each sample. y : np.ndarray An array containing the (monophonic) wave samples. steps : matplotlib.lines.Line2D The matplotlib artist used for the sample-based visualization. This is constructed by `matplotlib.pyplot.step`. envelope : matplotlib.collections.PolyCollection The matplotlib artist used for the envelope-based visualization. This is constructed by `matplotlib.pyplot.fill_between`. sr : number > 0 The sampling rate of the audio max_samples : int > 0 The maximum number of samples to use for sample-based display. transpose : bool If `True`, display the wave vertically instead of horizontally. label : str or None An optional label for the waveplot, used in legend entries. See Also -------- waveshow """ times: np.ndarray samples: np.ndarray sr: float max_samples: int transpose: bool cid: int | None label_proxy_: _WaveplotDecoy def __init__( self, times: np.ndarray, y: np.ndarray, steps: Line2D, envelope: PolyCollection, sr: float = 22050, max_samples: int = 11025, transpose: bool = False, label: str | None = None, ): self.times = times self.samples = y self._steps_ref = weakref.ref(steps) self._envelope_ref = weakref.ref(envelope) self.sr = sr self.max_samples = max_samples self.transpose = transpose self.cid = None self._ax_ref: weakref.ref[mplaxes.Axes] | None = None # This creates an invisible proxy artist to contain the label self.label_proxy_ = _WaveplotDecoy(self) self.label_proxy_.set_in_layout(False) if label is not None: self.label_proxy_.set_label(label) # Preserve the old attribute API by exposing properties with same names @property def steps(self) -> Line2D | None: """The step plot artist (Line2D), or None if garbage collected. Returns ------- Line2D or None The step plot artist, or ``None`` if it has been garbage collected. """ return self._steps_ref() @property def envelope(self) -> PolyCollection | None: """The envelope artist (PolyCollection), or None if garbage collected. Returns ------- PolyCollection or None The envelope artist, or ``None`` if it has been garbage collected. """ return self._envelope_ref() @property def ax(self) -> mplaxes.Axes | None: """The connected Axes, or None if not connected or garbage collected. Returns ------- matplotlib.axes.Axes or None The connected axes, or ``None`` if not connected or garbage collected. """ return None if self._ax_ref is None else self._ax_ref() def __del__(self) -> None: """Disconnect callback methods on delete""" self.disconnect(strict=True) def connect( self, ax: mplaxes.Axes, *, signal: str = "xlim_changed", ) -> None: """Connect the adaptor to a signal on an axes object. Note that if the adaptor has already been connected to an axes object, that connect is first broken and then replaced by a new callback. Parameters ---------- ax : matplotlib.axes.Axes The axes to connect with this adaptor's `update` signal : string, {"xlim_changed", "ylim_changed"} The signal to connect See Also -------- disconnect """ # Disconnect any existing callback first self.disconnect() # Attach to axes and store the connection id self._ax_ref = weakref.ref(ax) ax.add_artist(self.label_proxy_) self.cid = ax.callbacks.connect(signal, self.update) def disconnect(self, *, strict: bool = False) -> None: """Disconnect the adaptor's update callback. Parameters ---------- strict : bool If `True`, remove references to the connected axes. If `False` (default), only disconnect the callback. This functionality is intended primarily for internal use, and should have no observable effects for users. See Also -------- connect """ ax = self.ax if ax is not None and self.cid is not None: ax.callbacks.disconnect(self.cid) self.cid = None if strict: self._ax_ref = None def update(self, ax: mplaxes.Axes) -> None: """Update the matplotlib display according to the current viewport limits. This is a callback function, and should not be used directly. Parameters ---------- ax : matplotlib.axes.Axes The axes object to update """ # Deref artists and bail if they've been garbage collected steps = self.steps envelope = self.envelope if steps is None or envelope is None: return lims = ax.viewLim if self.transpose: dim = lims.height * self.sr start, end = lims.y0, lims.y1 xdata, ydata = self.samples, self.times data = steps.get_ydata() else: dim = lims.width * self.sr start, end = lims.x0, lims.x1 xdata, ydata = self.times, self.samples data = steps.get_xdata() # Does our width cover fewer than max_samples? # If so, then use the sample-based plot if dim <= self.max_samples: envelope.set_visible(False) steps.set_visible(True) # Now check our viewport # we have to squash mypy errors on operand compatibility # here because the type annotations from matplotlib are too # loose. if start <= data[0] or end >= data[-1]: # type: ignore[index] # Viewport expands beyond current data in steps; update # we want to cover a window of self.max_samples centered on the current viewport midpoint_time = (start + end) / 2 idx_start = np.searchsorted( self.times, midpoint_time - 0.5 * self.max_samples / self.sr ) steps.set_data( xdata[idx_start : idx_start + self.max_samples], ydata[idx_start : idx_start + self.max_samples], ) else: # Otherwise, use the envelope plot envelope.set_visible(True) steps.set_visible(False) ax.figure.canvas.draw_idle() class _WaveplotDecoy(mlines.Line2D): waveplot: AdaptiveWaveplot def __init__(self, parent_waveplot: AdaptiveWaveplot, *args: Any, **kwargs: Any): # We'll never actually set the color on this decoy at construction time kwargs["color"] = "none" super().__init__([], [], *args, **kwargs) self.waveplot = parent_waveplot # Store reference to the parent wrapper class _AdaptiveWaveplotHandler(HandlerBase): def create_artists(self, legend: Legend, orig_handle: Artist, xdescent: float, ydescent: float, width: float, height: float, fontsize: float, trans: mtransforms.Transform ) -> list[Artist]: """ Matplotlib automatically passes the exact dimensions and coordinate transform (`trans`) needed to paint safely inside the legend key box. """ orig_handle = cast("_WaveplotDecoy", orig_handle) waveplot = orig_handle.waveplot ax = waveplot.ax if ax is not None: bgcolor = ax.get_facecolor() else: bgcolor = "none" bg_rect = mpatches.Rectangle((0, 0), 1, 1, facecolor=bgcolor, edgecolor="none") bg_artists = HandlerPatch().create_artists( legend, bg_rect, xdescent, ydescent, width, height, fontsize, trans ) proxy_line = mlines.Line2D([], []) if waveplot.steps is not None: proxy_line.update_from(waveplot.steps) proxy_line.set_data([], []) proxy_line.set(visible=True) line_artists = HandlerLine2D().create_artists( legend, proxy_line, xdescent, ydescent, width, height, fontsize, trans ) return [*bg_artists, *line_artists] # Add our custom handler to the default legend handler map if _WaveplotDecoy not in Legend.get_default_handler_map(): Legend.update_default_handler_map({_WaveplotDecoy: _AdaptiveWaveplotHandler()}) class Transformf0(mtransforms.Transform): """A utility class to handle f0-displacement for waveform visualizations. Parameters ---------- f0 : np.ndarray Array of fundamental frequency values (in Hz), one per frame. Values may be NaN for unvoiced frames. sr : number > 0 Audio sampling rate, used with ``hop_length`` to compute time stamps. hop_length : int > 0 Number of audio samples between successive f0 frames. bins_per_octave : int > 0 Number of bins per octave used for the pitch axis. norm : float Normalization factor applied to the pitch axis. offset : float Time offset (in seconds) applied to the frame time stamps. transpose : bool If ``True``, the time axis is the second dimension instead of the first. is_inverted : bool If ``True``, apply the inverse of the f0-displacement transformation. """ f0_interp: scipy.interpolate.interp1d norm: float bins_per_octave: int f0: np.ndarray sr: float hop_length: int offset: float transpose: bool input_dims: int output_dims: int is_separable: bool is_inverted: bool def __init__( self, f0: np.ndarray, *, sr: float = 22050, hop_length: int = 512, bins_per_octave: int = 12, norm: float = 1, offset: float = 0, transpose: bool = False, is_inverted: bool = False, ): super().__init__(shorthand_name="Transformf0") if not np.any(np.isfinite(f0)) or np.nanmin(f0) <= 0: raise ParameterError("f0 must be strictly positive (or NaN) and contain at least one finite value") times = offset + core.times_like(f0, sr=sr, hop_length=hop_length) import scipy.interpolate self.f0_interp = scipy.interpolate.interp1d( times, f0, kind="previous", copy=False, bounds_error=False, assume_sorted=True, ) self.norm = norm self.bins_per_octave = bins_per_octave self.f0 = f0 self.sr = sr self.hop_length = hop_length self.offset = offset self.transpose = transpose self.input_dims = 2 self.output_dims = 2 self.is_separable = False self.is_inverted = is_inverted def transform_non_affine(self, values: ArrayLike) -> np.ndarray: """Apply the f0 displacement transformation to the given values. Parameters ---------- values : np.ndarray An array of shape (..., 2) containing time and sample values to be transformed. The order of time and sample values is determined by the `transpose` parameter of this class. Returns ------- output : np.ndarray An array of the same shape as `values`, containing the transformed time and sample values. """ values = np.asarray(values) if self.transpose: idx = (1, 0) else: idx = (0, 1) times = values[:, idx[0]] samples = values[:, idx[1]] output = np.empty_like(values) output[:, idx[0]] = times if self.is_inverted: output[:, idx[1]] = ( (np.log2(samples) - np.log2(self.f0_interp(times))) * self.norm * self.bins_per_octave ) else: output[:, idx[1]] = 2.0 ** ( samples / self.norm / self.bins_per_octave ) * self.f0_interp(times) return output def inverted(self) -> Transformf0: """Return the inverse of this transformation. Returns ------- Transformf0 A new ``Transformf0`` with ``is_inverted`` toggled. """ return Transformf0( f0=self.f0, sr=self.sr, hop_length=self.hop_length, bins_per_octave=self.bins_per_octave, norm=self.norm, offset=self.offset, transpose=self.transpose, is_inverted=not self.is_inverted, ) def infer_cmap( data: np.ndarray, *, robust: bool = True, cmap_seq: str | colors.Colormap = "magma", cmap_bool: str | colors.Colormap = "gray_r", cmap_div: str | colors.Colormap = "coolwarm", div_thresh: float = 0.0, ) -> Colormap: """Get a default colormap from the given data. If the data is boolean, use a black and white colormap. If the data has both positive and negative values, use a diverging colormap. Otherwise, use a sequential colormap. Parameters ---------- data : np.ndarray Input data robust : bool If True, discard the top and bottom 2% of data when calculating range. cmap_seq : str or matplotlib.colors.Colormap The sequential colormap cmap_bool : str or matplotlib.colors.Colormap The boolean colormap cmap_div : str or matplotlib.colors.Colormap The diverging colormap div_thresh : float The threshold for determining whether to use a diverging colormap. If the data has values both above and below this threshold, then a diverging colormap is used. Returns ------- cmap : matplotlib.colors.Colormap The colormap to use for ``data`` See Also -------- matplotlib.pyplot.colormaps """ data = np.atleast_1d(data) if not isinstance(cmap_seq, colors.Colormap): cmap_seq = mcm[cmap_seq] if not isinstance(cmap_bool, colors.Colormap): cmap_bool = mcm[cmap_bool] if not isinstance(cmap_div, colors.Colormap): cmap_div = mcm[cmap_div] if data.dtype.kind == "b": return cmap_bool data = data[np.isfinite(data)] if robust: min_p, max_p = 2, 98 else: min_p, max_p = 0, 100 min_val, max_val = np.percentile(data, [min_p, max_p]) if min_val >= div_thresh or max_val <= div_thresh: return cmap_seq return cmap_div # Deprecation rename of cmap -> infer_cmap for 1.0 cmap = moved(moved_from="librosa.display.cmap", version="1.0", version_removed="1.1")( infer_cmap ) def __envelope(x, hop): """Compute the max-envelope of non-overlapping frames of x at length hop x is assumed to be multi-channel, of shape (n_channels, n_samples). """ x_frame = np.abs(util.frame(x, frame_length=hop, hop_length=hop)) return x_frame.max(axis=1) _chroma_ax_types = ( "chroma", "chroma_h", "chroma_c", "chroma_fjs", ) _cqt_ax_types = ( "cqt_hz", "cqt_note", "cqt_svara", "cqt_oct3", ) _vqt_ax_types = ( "vqt_hz", "vqt_note", "vqt_oct3", "vqt_fjs", ) _freq_ax_types = ( "linear", "fft", "hz", "fft_note", "fft_svara", "oct3", ) _time_ax_types = ( "time", "h", "m", "s", "ms", ) _lag_ax_types = ( "lag", "lag_h", "lag_m", "lag_s", "lag_ms", ) _misc_ax_types = ( "tempo", "fourier_tempo", "mel", "mel_oct3", "log", "tonnetz", "frames", ) _AXIS_COMPAT = set( [(t, t) for t in _misc_ax_types] + [t for t in product(_chroma_ax_types, _chroma_ax_types)] + [t for t in product(_cqt_ax_types, _cqt_ax_types)] + [t for t in product(_vqt_ax_types, _vqt_ax_types)] + [t for t in product(_freq_ax_types, _freq_ax_types)] + [t for t in product(_time_ax_types, _time_ax_types)] + [t for t in product(_lag_ax_types, _lag_ax_types)] ) def specshow( data: np.ndarray, *, x_coords: np.ndarray | None = None, y_coords: np.ndarray | None = None, x_axis: str | None = None, y_axis: str | None = None, vscale: str | None = None, sr: float = 22050, hop_length: int = 512, n_fft: int | None = None, win_length: int | None = None, fmin: float | None = None, fmax: float | None = None, tempo_min: float | None = 16, tempo_max: float | None = 480, tuning: float = 0.0, bins_per_octave: int = 12, key: str = "C:maj", Sa: float | None = None, mela: str | int | None = None, thaat: str | None = None, auto_aspect: bool = True, htk: bool = False, unicode: bool = True, intervals: str | np.ndarray | None = None, unison: str | None = None, top_db: float | None = 80.0, cmap_seq: str | colors.Colormap = "magma", cmap_bool: str | colors.Colormap = "gray_r", cmap_div: str | colors.Colormap = "coolwarm", cmap_cyclic: str | colors.Colormap = "twilight_shifted", div_thresh: float = 0.0, ax: mplaxes.Axes | None = None, **kwargs: Any, ) -> QuadMesh: """Display a spectrogram/chromagram/cqt/etc. For a detailed overview of display functionality, see the :ref:`Display and visualization tutorial `. Parameters ---------- data : np.ndarray [shape=(d, n)] Matrix to display (e.g., spectrogram) x_coords, y_coords : np.ndarray [shape=data.shape[0 or 1]] Optional positioning coordinates of the input data. These can be use to explicitly set the location of each element ``data[i, j]``, e.g., for displaying beat-synchronous features in natural time coordinates. If not provided, they are inferred from ``x_axis`` and ``y_axis``. x_axis, y_axis : None or str Range for the x- and y-axes. Valid types are: - None, 'none', or 'off' : no axis decoration is displayed. Frequency types: - 'linear', 'fft', 'hz' : frequency range is determined by the FFT window and sampling rate. - 'log' : the spectrum is displayed on a log scale. - 'oct3' : the spectrum is displayed on a log scale with frequencies marked in scientific notation at 1/3-octave intervals - 'fft_note': the spectrum is displayed on a log scale with pitches marked. - 'fft_svara': the spectrum is displayed on a log scale with svara marked. - 'mel' : frequencies are determined by the mel scale. - 'mel_oct3' : like 'oct3' above, but using the mel scale. - 'cqt_hz' : frequencies are determined by the CQT scale. - 'cqt_oct3' : like 'oct3' above, but using the CQT scale. - 'cqt_note' : pitches are determined by the CQT scale. - 'cqt_svara' : like `cqt_note` but using Hindustani or Carnatic svara - 'vqt_hz' : like `cqt_hz` but using Variable-Q Transform (VQT) scale. - 'vqt_oct3' : like 'oct3' above, but using the VQT scale. - 'vqt_fjs' : like `cqt_note` but using Functional Just System (FJS) notation. This requires a just intonation-based variable-Q transform representation. - 'vqt_note' : like 'cqt_note' but using the VQT scale. All frequency types are plotted in units of Hz. `oct3`-type use SI prefixes for frequencies, e.g., `1 kHz`, `2 MHz`, and are well adapted for scientific applications using high-frequency data. .. note:: The 'log', 'fft_note', 'fft_svara', 'log_oct3', 'mel', and 'mel_oct3' axes use symmetric-log scaling to retain frequency bins near 0 Hz. CQT and VQT axes use logarithmic scaling. Any spectrogram parameters (hop_length, sr, bins_per_octave, etc.) used to generate the input data should also be provided when calling `specshow`. Categorical types: - 'chroma' : pitches are determined by the chroma filters. Pitch classes are arranged at integer locations (0-11) according to a given key. - `chroma_h`, `chroma_c`: pitches are determined by chroma filters, and labeled as svara in the Hindustani (`chroma_h`) or Carnatic (`chroma_c`) according to a given thaat (Hindustani) or melakarta raga (Carnatic). - 'chroma_fjs': pitches are determined by chroma filters using just intonation. All pitch classes are annotated. - 'tonnetz' : axes are labeled by Tonnetz dimensions (0-5) - 'frames' : markers are shown as frame counts. Time types: - 'time' : markers are shown as milliseconds, seconds, minutes, or hours. Values are plotted in units of seconds. - 'h' : markers are shown as hours, minutes, and seconds. - 'm' : markers are shown as minutes and seconds. - 's' : markers are shown as seconds. - 'ms' : markers are shown as milliseconds. - 'lag' : like time, but past the halfway point counts as negative values. - 'lag_h' : same as lag, but in hours, minutes and seconds. - 'lag_m' : same as lag, but in minutes and seconds. - 'lag_s' : same as lag, but in seconds. - 'lag_ms' : same as lag, but in milliseconds. Rhythm: - 'tempo' : markers are shown as beats-per-minute (BPM) using a logarithmic scale. This is useful for visualizing the outputs of `feature.tempogram`. - 'fourier_tempo' : same as `'tempo'`, but used when tempograms are calculated in the Frequency domain using `feature.fourier_tempogram`. vscale : str Optional value transformation for `data`. The following are supported: - 'dB' : decibels with `1` as a reference amplitude - 'dB[]' : decibels with the given value as a reference amplitude, e.g. 'dB[0.1]'. - 'dB[power]' : like above, but treating `data` as power rather than amplitude measurements. - 'dB[power,]' : like above, but with an explicit reference power value, e.g. 'dB[power,0.1]'. - 'dBFS' : decibels relative to full scale, using `np.max(data)` as a reference amplitude - 'dBFS[power]' : like above, but treating `data` as power rather than amplitude measurements. - 'phase' : phase values in radians, with a range of `[-Ï€, Ï€]`. - 'dphase' : unwrapped phase differences in radians. Each pixel corresponds to the residual between the observed phase and the expected phase if the frequency was stationary at the previous time step. Values are in the range of `[-Ï€, Ï€]`. - 'dphase_t' : as above, but differences are computed along the vertical axis instead of horizontal. This is intended for use with transposed spectrograms where the time axis is vertical and the frequency axis is horizontal. .. note:: When using phase difference modes (`dphase` or `dphase_t`), the x and y coordinates must be provided via either the `x_axis` and `y_axis` parameters (e.g., `'time', 'fft'`), or explicitly by the `x_coords` and `y_coords` parameters. All time-like and frequency-like axes are supported. sr : number > 0 [scalar] Sample rate used to determine time scale in x-axis. hop_length : int > 0 [scalar] Hop length, also used to determine time scale in x-axis n_fft : int > 0 or None Number of samples per frame in STFT/spectrogram displays. By default, this will be inferred from the shape of ``data`` as ``2 * (d - 1)``. If ``data`` was generated using an odd frame length, the correct value can be specified here. win_length : int > 0 or None The number of samples per window. By default, this will be inferred to match ``n_fft``. This is primarily useful for specifying odd window lengths in Fourier tempogram displays. fmin : float > 0 [scalar] or None Frequency of the lowest spectrogram bin. Used for Mel, CQT, and VQT scales. If ``y_axis`` is `cqt_hz` or `cqt_note` and ``fmin`` is not given, it is set by default to ``note_to_hz('C1')``. fmax : float > 0 [scalar] or None Used for setting the Mel frequency scales tempo_min : float > 0 [scalar] Lowest tempo (in beats per minute) for tempogram display. tempo_max : float > 0 [scalar] Highest tempo (in beats per minute) for tempogram display. tuning : float Tuning deviation from A440, in fractions of a bin. This is used for CQT frequency scales, so that ``fmin`` is adjusted to ``fmin * 2**(tuning / bins_per_octave)``. bins_per_octave : int > 0 [scalar] Number of bins per octave. Used for CQT frequency scale. key : str The reference key to use when using note axes (`cqt_note`, `chroma`). Sa : float or int If using Hindustani or Carnatic svara axis decorations, specify Sa. For `cqt_svara`, ``Sa`` should be specified as a frequency in Hz. For `chroma_c` or `chroma_h`, ``Sa`` should correspond to the position of Sa within the chromagram. If not provided, Sa will default to 0 (equivalent to `C`) mela : str or int, optional If using `chroma_c` or `cqt_svara` display mode, specify the melakarta raga. thaat : str, optional If using `chroma_h` display mode, specify the parent thaat. auto_aspect : bool Axes will have 'equal' aspect if the horizontal and vertical dimensions cover the same extent and their types match. To override, set to `False`. htk : bool If plotting on a mel frequency axis, specify which version of the mel scale to use. - `False`: use Slaney formula (default) - `True`: use HTK formula See `core.mel_frequencies` for more information. unicode : bool If using note or svara decorations, setting `unicode=True` will use unicode glyphs for accidentals and octave encoding. Setting `unicode=False` will use ASCII glyphs. This can be helpful if your font does not support musical notation symbols. intervals : str or array of floats in [1, 2), optional If using an FJS notation (`chroma_fjs`, `vqt_fjs`), the interval specification. See `core.interval_frequencies` for a description of supported values. unison : str, optional If using an FJS notation (`chroma_fjs`, `vqt_fjs`), the pitch name of the unison interval. If not provided, it will be inferred from `fmin` (for VQT display) or assumed as `'C'` (for chroma display). top_db : float If using a decibel scale, how many dB below the peak to allow before clipping. cmap_seq : str or matplotlib.colors.Colormap The name of the sequential colormap to use for decibel scales. Default is 'magma'. cmap_bool : str or matplotlib.colors.Colormap The name of the colormap to use for boolean data. Default is 'gray_r'. cmap_div : str or matplotlib.colors.Colormap The name of the diverging colormap to use for diverging data. Default is 'coolwarm'. cmap_cyclic : str or matplotlib.colors.Colormap The name of the cyclic colormap to use for phase data. Default is 'twilight_shifted'. div_thresh : float The threshold for determining whether to use a diverging colormap. If the data has values both above and below this threshold, then a diverging colormap is used. ax : matplotlib.axes.Axes or None Axes to plot on instead of the default `plt.gca()`. **kwargs : additional keyword arguments Arguments passed through to `matplotlib.pyplot.pcolormesh`. By default, the following options are set: - ``rasterized=True`` - ``shading='auto'`` - ``edgecolors='None'`` Notes ----- The ``cmap`` option if not provided via `kwargs`, is inferred from data automatically. If `vscale` is specified, the colormap will be sequential for decibels, and cyclic for phase and phase differences. If a diverging colormap is inferred, the color scale is normalized so that the center value (``div_thresh=0`` by default) is at the center of the colormap. To use matplotlib's default colormap, explicitly set ``cmap=None``. Returns ------- colormesh : `matplotlib.collections.QuadMesh` The color mesh object produced by `matplotlib.pyplot.pcolormesh` See Also -------- colorbar_db colorbar_phase infer_cmap : Automatic colormap detection matplotlib.pyplot.pcolormesh Examples -------- Visualize an STFT magnitude spectrum using default parameters >>> import matplotlib.pyplot as plt >>> y, sr = librosa.loadx('choice', duration=15) >>> fig, ax = plt.subplots(nrows=2, ncols=1, sharex=True) >>> D = librosa.stft(y) >>> img = librosa.display.specshow(D, y_axis='linear', x_axis='time', ... vscale='dBFS', sr=sr, ax=ax[0]) >>> ax[0].set(title='Linear-frequency magnitude spectrogram') >>> ax[0].label_outer() Or on a logarithmic scale, and using a larger hop >>> hop_length = 1024 >>> D = librosa.stft(y, hop_length=hop_length) >>> librosa.display.specshow(D, y_axis='log', sr=sr, hop_length=hop_length, ... vscale='dBFS', x_axis='time', ax=ax[1]) >>> ax[1].set(title='Log-frequency magnitude spectrogram') >>> ax[1].label_outer() >>> librosa.display.colorbar_db(img, ax=ax) """ all_params = dict( kwargs=kwargs, sr=sr, fmin=fmin, fmax=fmax, tuning=tuning, bins_per_octave=bins_per_octave, hop_length=hop_length, n_fft=n_fft, win_length=win_length, key=key, htk=htk, unicode=unicode, intervals=intervals, unison=unison, ) # Get the x and y coordinates y_coords = __mesh_coords(y_axis, y_coords, data.shape[0], **all_params) x_coords = __mesh_coords(x_axis, x_coords, data.shape[1], **all_params) # Parse the value scale into a normalizer and possibly a colormap data, norm_cmap = __scale_data( data, vscale=vscale, top_db=top_db, x_coords=x_coords, y_coords=y_coords, cmap_seq=cmap_seq, cmap_cyclic=cmap_cyclic, ) if np.issubdtype(data.dtype, np.complexfloating): warnings.warn( "Trying to display complex-valued input. Showing magnitude instead.", stacklevel=2, ) data = np.abs(data) if norm_cmap is not None: kwargs.setdefault("cmap", norm_cmap) elif "cmap" not in kwargs: # Neither vscale nor the user gave us a cmap, so we have to infer it kwargs["cmap"] = infer_cmap( data, cmap_seq=cmap_seq, cmap_bool=cmap_bool, cmap_div=cmap_div, div_thresh=div_thresh, ) if isinstance(cmap_div, colors.Colormap): is_diverging_cmap = kwargs["cmap"] == cmap_div else: is_diverging_cmap = kwargs["cmap"] == mcm.get(cmap_div, None) if isinstance(cmap_bool, colors.Colormap): is_boolean_cmap = kwargs["cmap"] == cmap_bool else: is_boolean_cmap = kwargs["cmap"] == mcm.get(cmap_bool, None) # Harden this check to ensure that it only hits when # data is really boolean is_boolean_cmap &= (data.dtype.kind == "b") if is_diverging_cmap: # If we have an inferred diverging colormap, # use a twoslope normalizer around the divergence threshold. # But only if the user didn't also set their own normalizer # If the user gave vmin/vmax values, move them from kwargs to the norm kwargs.setdefault( "norm", colors.TwoSlopeNorm( vcenter=div_thresh, vmin=kwargs.pop("vmin", None), vmax=kwargs.pop("vmax", None), ), ) elif is_boolean_cmap: # If we have an inferred boolean colormap, use a boundary norm # But only if the user didn't also set their own normalizer kwargs.setdefault( "norm", colors.BoundaryNorm( boundaries=[0, 0.5, 1], ncolors=kwargs["cmap"].N ), ) kwargs.setdefault("rasterized", True) kwargs.setdefault("edgecolors", "None") kwargs.setdefault("shading", "auto") if vscale is not None and "phase" in vscale: # If we're displaying phase, try to ensure that the color gamut # covers the full range. # A user can override this if they want to. kwargs.setdefault("vmin", -np.pi) kwargs.setdefault("vmax", np.pi) axes = __check_axes(ax) out = axes.pcolormesh(x_coords, y_coords, data, **kwargs) __set_current_image(ax, out) # Set up axis scaling __scale_axes(axes, x_axis, "x", tempo_min=tempo_min, tempo_max=tempo_max) __scale_axes(axes, y_axis, "y", tempo_min=tempo_min, tempo_max=tempo_max) # Construct tickers and locators __decorate_axis( axes.xaxis, x_axis, key=key, Sa=Sa, mela=mela, thaat=thaat, unicode=unicode, fmin=fmin, unison=unison, intervals=intervals, bins_per_octave=bins_per_octave, n_bins=len(x_coords), ) __decorate_axis( axes.yaxis, y_axis, key=key, Sa=Sa, mela=mela, thaat=thaat, unicode=unicode, fmin=fmin, unison=unison, intervals=intervals, bins_per_octave=bins_per_octave, n_bins=len(y_coords), ) # If the plot is a self-similarity/covariance etc. plot, square it if __same_axes(x_axis, y_axis, axes.get_xlim(), axes.get_ylim()) and auto_aspect: axes.set_aspect("equal") return out def __set_current_image(ax, img): """ Set the current image when working in pyplot mode. If the provided ``ax`` is not `None`, then we assume that the user is using the object API. In this case, the pyplot current image is not set. """ if ax is None: plt.sci(img) def __mesh_coords(ax_type, coords, n, **kwargs): """Compute axis coordinates""" if coords is not None: if len(coords) not in (n, n + 1): raise ParameterError( f"Coordinate shape mismatch: {len(coords)}!={n} or {n}+1" ) return coords coord_map: dict[str | None, Callable[..., np.ndarray]] = { "linear": __coord_fft_hz, "fft": __coord_fft_hz, "fft_note": __coord_fft_hz, "fft_svara": __coord_fft_hz, "hz": __coord_fft_hz, "oct3": __coord_fft_hz, "log_oct3": __coord_fft_hz, "log": __coord_fft_hz, "mel": __coord_mel_hz, "mel_oct3": __coord_mel_hz, "cqt": __coord_cqt_hz, "cqt_hz": __coord_cqt_hz, "cqt_note": __coord_cqt_hz, "cqt_svara": __coord_cqt_hz, "cqt_oct3": __coord_cqt_hz, "vqt_fjs": __coord_vqt_hz, "vqt_hz": __coord_vqt_hz, "vqt_note": __coord_vqt_hz, "vqt_oct3": __coord_vqt_hz, "chroma": __coord_chroma, "chroma_c": __coord_chroma, "chroma_h": __coord_chroma, "chroma_fjs": __coord_n, # We can't use a 12-normalized tick locator here "time": __coord_time, "h": __coord_time, "m": __coord_time, "s": __coord_time, "ms": __coord_time, "lag": __coord_time, "lag_h": __coord_time, "lag_m": __coord_time, "lag_s": __coord_time, "lag_ms": __coord_time, "tonnetz": __coord_n, "off": __coord_n, "tempo": __coord_tempo, "fourier_tempo": __coord_fourier_tempo, "frames": __coord_n, None: __coord_n, } if ax_type not in coord_map: raise ParameterError(f"Unknown axis type: {ax_type}") return coord_map[ax_type](n, **kwargs) def __check_axes(axes: mplaxes.Axes | None) -> mplaxes.Axes: """Check if "axes" is an instance of an axis object. If not, use `gca`.""" if axes is None: axes = plt.gca() elif not isinstance(axes, mplaxes.Axes): raise ParameterError( "`axes` must be an instance of matplotlib.axes.Axes. " "Found type(axes)={}".format(type(axes)) ) return axes def __scale_axes(axes, ax_type, which, tempo_min, tempo_max): """Set the axis scaling""" kwargs = dict() thresh = "linthresh" base = "base" scale = "linscale" if which == "x": scaler = axes.set_xscale limit = axes.set_xlim else: scaler = axes.set_yscale limit = axes.set_ylim # Map ticker scales if ax_type in ["mel", "mel_oct3"]: mode = "symlog" kwargs[thresh] = 1000.0 kwargs[base] = 2 elif ax_type in [ "cqt", "cqt_hz", "cqt_note", "cqt_svara", "cqt_oct3", "vqt_hz", "vqt_note", "vqt_fjs", "vqt_oct3", ]: mode = "log" kwargs[base] = 2 elif ax_type in ["log", "fft_note", "fft_svara", "log_oct3"]: mode = "symlog" kwargs[base] = 2 kwargs[thresh] = float(core.note_to_hz("C2")) kwargs[scale] = 0.5 elif ax_type in ["tempo", "fourier_tempo"]: mode = "log" kwargs[base] = 2 limit(tempo_min, tempo_max) else: return scaler(mode, **kwargs) def __decorate_axis( axis, ax_type, key="C:maj", Sa=None, mela=None, thaat=None, unicode=True, fmin=None, unison=None, intervals=None, bins_per_octave=None, n_bins=None, ): """Configure axis tickers, locators, and labels""" time_units = {"h": "hours", "m": "minutes", "s": "seconds", "ms": "milliseconds"} if ax_type == "tonnetz": axis.set_major_formatter(TonnetzFormatter()) axis.set_major_locator(mplticker.FixedLocator([0, 1, 2, 3, 4, 5])) axis.set_label_text("Tonnetz") elif ax_type == "chroma": axis.set_major_formatter(ChromaFormatter(key=key, unicode=unicode)) degrees = core.key_to_degrees(key) axis.set_major_locator( mplticker.FixedLocator( cast( "Sequence[float]", np.add.outer(12 * np.arange(10), degrees, dtype=float).ravel(), ) ) ) axis.set_label_text("Pitch class") elif ax_type == "chroma_h": if Sa is None: Sa = 0 axis.set_major_formatter(ChromaSvaraFormatter(Sa=Sa, unicode=unicode)) if thaat is None: # If no thaat is given, show all svara degrees = np.arange(12) else: degrees = core.thaat_to_degrees(thaat) # Rotate degrees relative to Sa degrees = np.mod(degrees + Sa, 12) axis.set_major_locator( mplticker.FixedLocator( cast( "Sequence[float]", np.add.outer(12 * np.arange(10), degrees, dtype=float).ravel(), ) ) ) axis.set_label_text("Svara") elif ax_type == "chroma_c": if Sa is None: Sa = 0 axis.set_major_formatter( ChromaSvaraFormatter(Sa=Sa, mela=mela, unicode=unicode) ) degrees = core.mela_to_degrees(mela) # Rotate degrees relative to Sa degrees = np.mod(degrees + Sa, 12) axis.set_major_locator( mplticker.FixedLocator( cast( "Sequence[float]", np.add.outer(12 * np.arange(10), degrees, dtype=float).ravel(), ) ) ) axis.set_label_text("Svara") elif ax_type == "chroma_fjs": if fmin is None: fmin = core.note_to_hz("C1") if unison is None: unison = core.hz_to_note(fmin, octave=False, cents=False) axis.set_major_formatter( ChromaFJSFormatter( intervals=intervals, unison=unison, unicode=unicode, bins_per_octave=bins_per_octave, ) ) if isinstance(intervals, str) and bins_per_octave > 7: # If intervals are implicit, generate the first 7 and identify # them in the sorted set tick_intervals = core.interval_frequencies( 7, fmin=1, intervals=intervals, bins_per_octave=bins_per_octave, sort=False, ) all_intervals = core.interval_frequencies( bins_per_octave, fmin=1, intervals=intervals, bins_per_octave=bins_per_octave, sort=True, ) degrees = util.match_events(tick_intervals, all_intervals) else: # If intervals are explicit, tick them all degrees = np.arange(bins_per_octave) axis.set_major_locator(mplticker.FixedLocator(degrees)) # type: ignore[arg-type] axis.set_label_text("Pitch class") elif ax_type in ["tempo", "fourier_tempo"]: axis.set_major_formatter(mplticker.ScalarFormatter()) axis.set_major_locator(mplticker.LogLocator(base=2.0)) axis.set_label_text("BPM") elif ax_type == "time": axis.set_major_formatter(TimeFormatter(unit=None, lag=False)) axis.set_major_locator( mplticker.MaxNLocator(prune=None, steps=[1, 1.5, 5, 6, 10]) ) axis.set_label_text("Time") elif ax_type in time_units: axis.set_major_formatter(TimeFormatter(unit=ax_type, lag=False)) axis.set_major_locator( mplticker.MaxNLocator(prune=None, steps=[1, 1.5, 5, 6, 10]) ) axis.set_label_text("Time ({:s})".format(time_units[ax_type])) elif ax_type == "lag": axis.set_major_formatter(TimeFormatter(unit=None, lag=True)) axis.set_major_locator( mplticker.MaxNLocator(prune=None, steps=[1, 1.5, 5, 6, 10]) ) axis.set_label_text("Lag") elif isinstance(ax_type, str) and ax_type.startswith("lag_"): unit = ax_type[4:] axis.set_major_formatter(TimeFormatter(unit=unit, lag=True)) axis.set_major_locator( mplticker.MaxNLocator(prune=None, steps=[1, 1.5, 5, 6, 10]) ) axis.set_label_text("Lag ({:s})".format(time_units[unit])) elif ax_type == "cqt_note": axis.set_major_formatter(NoteFormatter(key=key, unicode=unicode)) # Where is C1 relative to 2**k hz? log_C1 = np.log2(core.note_to_hz("C1")) C_offset = 2.0 ** (log_C1 - np.floor(log_C1)) axis.set_major_locator(mplticker.LogLocator(base=2.0, subs=(C_offset,))) axis.set_minor_formatter(NoteFormatter(key=key, major=False, unicode=unicode)) axis.set_minor_locator( mplticker.LogLocator( base=2.0, subs=C_offset * 2.0 ** (np.arange(1, 12) / 12.0) ) ) axis.set_label_text("Note") elif ax_type == "cqt_svara": axis.set_major_formatter(SvaraFormatter(Sa=Sa, mela=mela, unicode=unicode)) # Find the offset of Sa relative to 2**k Hz sa_offset = 2.0 ** (np.log2(Sa) - np.floor(np.log2(Sa))) axis.set_major_locator(mplticker.LogLocator(base=2.0, subs=(sa_offset,))) axis.set_minor_formatter( SvaraFormatter(Sa=Sa, mela=mela, major=False, unicode=unicode) ) axis.set_minor_locator( mplticker.LogLocator( base=2.0, subs=sa_offset * 2.0 ** (np.arange(1, 12) / 12.0) ) ) axis.set_label_text("Svara") elif ax_type == "vqt_fjs": if fmin is None: fmin = float(core.note_to_hz("C1")) axis.set_major_formatter( FJSFormatter( intervals=intervals, fmin=fmin, unison=unison, unicode=unicode, bins_per_octave=bins_per_octave, n_bins=n_bins, ) ) log_fmin = np.log2(fmin) fmin_offset = 2.0 ** (log_fmin - np.floor(log_fmin)) axis.set_major_locator(mplticker.LogLocator(base=2.0, subs=(fmin_offset,))) axis.set_minor_formatter( FJSFormatter( intervals=intervals, fmin=fmin, unison=unison, unicode=unicode, bins_per_octave=bins_per_octave, n_bins=n_bins, major=False, ) ) axis.set_minor_locator( mplticker.FixedLocator( core.interval_frequencies( n_bins * 12 // bins_per_octave, fmin=fmin, intervals=intervals, bins_per_octave=12, ) # type: ignore[arg-type] ) ) axis.set_label_text("Note") elif ax_type == "vqt_hz": if fmin is None: fmin = core.note_to_hz("C1") axis.set_major_formatter(LogHzFormatter()) log_fmin = np.log2(fmin) fmin_offset = 2.0 ** (log_fmin - np.floor(log_fmin)) axis.set_major_locator(mplticker.LogLocator(base=2.0, subs=(fmin_offset,))) axis.set_minor_formatter(LogHzFormatter(major=False)) axis.set_minor_locator( mplticker.LogLocator( base=2.0, subs=core.interval_frequencies( 12, fmin=fmin_offset, intervals=intervals, bins_per_octave=12 ), # type: ignore[arg-type] ) ) axis.set_label_text("Hz") elif ax_type == "vqt_note": if fmin is None: fmin = core.note_to_hz("C1") axis.set_major_formatter(NoteFormatter(key=key, unicode=unicode)) log_fmin = np.log2(fmin) fmin_offset = 2.0 ** (log_fmin - np.floor(log_fmin)) axis.set_major_locator(mplticker.LogLocator(base=2.0, subs=(fmin_offset,))) axis.set_minor_formatter(NoteFormatter(key=key, unicode=unicode, major=False)) axis.set_minor_locator( mplticker.LogLocator( base=2.0, subs=core.interval_frequencies( 12, fmin=fmin_offset, intervals=intervals, bins_per_octave=12 ), # type: ignore[arg-type] ) ) axis.set_label_text("Note") elif ax_type in ["cqt_hz"]: axis.set_major_formatter(LogHzFormatter()) log_C1 = np.log2(core.note_to_hz("C1")) C_offset = 2.0 ** (log_C1 - np.floor(log_C1)) axis.set_major_locator(mplticker.LogLocator(base=2.0, subs=(C_offset,))) axis.set_major_locator(mplticker.LogLocator(base=2.0)) axis.set_minor_formatter(LogHzFormatter(major=False)) axis.set_minor_locator( mplticker.LogLocator( base=2.0, subs=C_offset * 2.0 ** (np.arange(1, 12) / 12.0) ) ) axis.set_label_text("Hz") elif ax_type == "fft_note": axis.set_major_formatter(NoteFormatter(key=key, unicode=unicode)) # Where is C1 relative to 2**k hz? log_C1 = np.log2(core.note_to_hz("C1")) C_offset = 2.0 ** (log_C1 - np.floor(log_C1)) axis.set_major_locator(mplticker.SymmetricalLogLocator(axis.get_transform())) axis.set_minor_formatter(NoteFormatter(key=key, major=False, unicode=unicode)) axis.set_minor_locator( mplticker.LogLocator(base=2.0, subs=2.0 ** (np.arange(1, 12) / 12.0)) # type: ignore[arg-type] ) axis.set_label_text("Note") elif ax_type == "fft_svara": axis.set_major_formatter(SvaraFormatter(Sa=Sa, mela=mela, unicode=unicode)) # Find the offset of Sa relative to 2**k Hz log_Sa = np.log2(Sa) sa_offset = 2.0 ** (log_Sa - np.floor(log_Sa)) axis.set_major_locator( mplticker.SymmetricalLogLocator( axis.get_transform(), base=2.0, subs=[sa_offset] ) ) axis.set_minor_formatter( SvaraFormatter(Sa=Sa, mela=mela, major=False, unicode=unicode) ) axis.set_minor_locator( mplticker.LogLocator( base=2.0, subs=sa_offset * 2.0 ** (np.arange(1, 12) / 12.0) ) ) axis.set_label_text("Svara") elif ax_type in ["mel", "log"]: axis.set_major_formatter(mplticker.ScalarFormatter()) axis.set_major_locator(mplticker.SymmetricalLogLocator(axis.get_transform())) axis.set_label_text("Hz") elif ax_type in ["linear", "hz", "fft"]: axis.set_major_formatter(mplticker.ScalarFormatter()) axis.set_label_text("Hz") elif ax_type in ["oct3", "cqt_oct3", "vqt_oct3", "log_oct3", "mel_oct3"]: # Label once per octave if ax_type == "mel_oct3": # Suppress major ticks for frequencies below 100 Hz in mel mode axis.set_major_locator(mplticker.FixedLocator(__OCT3_FREQUENCIES[5::3])) # type: ignore[arg-type] else: axis.set_major_locator(mplticker.FixedLocator(__OCT3_FREQUENCIES[::3])) # type: ignore[arg-type] axis.set_major_formatter(AdaptiveEngFormatter(major=True, unit="Hz")) axis.set_label_text("Frequency") # Minor ticks at the 1/3 octaves axis.set_minor_locator(mplticker.FixedLocator(__OCT3_FREQUENCIES, nbins=None)) # type: ignore[arg-type] axis.set_minor_formatter(AdaptiveEngFormatter(major=False, unit="Hz")) elif ax_type in ["frames"]: axis.set_label_text("Frames") elif ax_type in ["off", "none", None]: axis.set_label_text("") axis.set_ticks([]) else: raise ParameterError(f"Unsupported axis type: {ax_type}") def __coord_fft_hz( n: int, sr: float = 22050, n_fft: int | None = None, **_kwargs: Any ) -> _Array1D[np.float64]: """Get the frequencies for FFT bins""" if n_fft is None: n_fft = 2 * (n - 1) # The following code centers the FFT bins at their frequencies # and clips to the non-negative frequency range [0, nyquist] basis = core.fft_frequencies(sr=sr, n_fft=n_fft) return basis def __coord_mel_hz( n: int, fmin: float | None = 0.0, fmax: float | None = None, sr: float = 22050, htk: bool = False, **_kwargs: Any, ) -> _Array1D[np.float64]: """Get the frequencies for Mel bins""" if fmin is None: fmin = 0.0 if fmax is None: fmax = 0.5 * sr basis = core.mel_frequencies(n, fmin=fmin, fmax=fmax, htk=htk) return basis def __coord_cqt_hz( n: int, fmin: _FloatLike_co | None = None, bins_per_octave: int = 12, sr: float = 22050, **_kwargs: Any, ) -> _Array1D[np.float64]: """Get CQT bin frequencies""" if fmin is None: fmin = core.note_to_hz("C1") # Apply tuning correction fmin = fmin * 2.0 ** (_kwargs.get("tuning", 0.0) / bins_per_octave) # we drop by half a bin so that CQT bins are centered vertically freqs = core.cqt_frequencies( n, fmin=fmin, bins_per_octave=bins_per_octave, ) if np.any(freqs > 0.5 * sr): warnings.warn( "Frequency axis exceeds Nyquist. " "Did you remember to set all spectrogram parameters in specshow?", stacklevel=4, ) return freqs def __coord_vqt_hz( n: int, fmin: _FloatLike_co | None = None, bins_per_octave: int = 12, sr: float = 22050, intervals: str | Collection[float] | None = None, unison: str | None = None, **_kwargs: Any, ) -> _Array1D[np.float64]: if fmin is None: fmin = core.note_to_hz("C1") if intervals is None: raise ParameterError("VQT axis coordinates cannot be defined without intervals") freqs = core.interval_frequencies( n, fmin=fmin, intervals=intervals, bins_per_octave=bins_per_octave ) if np.any(freqs > 0.5 * sr): warnings.warn( "Frequency axis exceeds Nyquist. " "Did you remember to set all spectrogram parameters in specshow?", stacklevel=4, ) return freqs def __coord_chroma(n: int, bins_per_octave: int = 12, **_kwargs: Any) -> np.ndarray: """Get chroma bin numbers""" return np.linspace(0, (12.0 * n) / bins_per_octave, num=n, endpoint=False) def __coord_tempo( n: int, sr: float = 22050, hop_length: int = 512, **_kwargs: Any ) -> np.ndarray: """Tempo coordinates""" basis = core.tempo_frequencies(n + 1, sr=sr, hop_length=hop_length)[1:] return basis def __coord_fourier_tempo( n: int, sr: float = 22050, hop_length: int = 512, win_length: int | None = None, **_kwargs: Any, ) -> np.ndarray: """Fourier tempogram coordinates""" if win_length is None: win_length = 2 * (n - 1) # The following code centers the FFT bins at their frequencies # and clips to the non-negative frequency range [0, nyquist] basis = core.fourier_tempo_frequencies( sr=sr, hop_length=hop_length, win_length=win_length ) return basis def __coord_n(n: int, **_kwargs: Any) -> np.ndarray: """Get bare positions""" return np.arange(n) def __coord_time( n: int, sr: float = 22050, hop_length: int = 512, **_kwargs: Any ) -> np.ndarray: """Get time coordinates from frames""" times: np.ndarray = core.frames_to_time(np.arange(n), sr=sr, hop_length=hop_length) return times def __same_axes(x_axis, y_axis, xlim, ylim): """Check if two axes are similar, used to determine squared plots""" axes_compatible_and_not_none = (x_axis, y_axis) in _AXIS_COMPAT axes_same_lim = xlim == ylim return axes_compatible_and_not_none and axes_same_lim def __scale_data(data, *, vscale, top_db, x_coords, y_coords, cmap_seq, cmap_cyclic): """Parse the vscale parameter and return the transformed data and colormap if necessary Parameters ---------- data : np.ndarray The data to be scaled and visualized. vscale : str or None The value scale to apply to the data. If None, the data is returned as-is. top_db : float The maximum decibel level to display when using a dB scale. This is only used if `vscale` is set to a dB mode. x_coords, y_coords : np.ndarray Time and frequency coordinates for the data. These should be constructed using the `__mesh_coords` function. cmap_seq : str or matplotlib.colors.Colormap Default sequential colormap to use for dB scales. cmap_cyclic : str or matplotlib.colors.Colormap Default cyclic colormap to use for phase scales. Returns ------- data : np.ndarray The scaled data, ready for visualization. cmap : matplotlib.colors.Colormap or None The colormap to use for visualization, or None if no scaling is applied. """ # If vscale is None, we return the data as-is if vscale is None: return data, None # First check for the easy cases if vscale == "phase": # Phase should use a cyclic colormap return np.angle(data), cmap_cyclic elif vscale == "dphase": # Compute the difference of unwrapped phase diff = np.diff(np.unwrap(np.angle(data), axis=-1), axis=-1, prepend=0.0) # Correct it compared to the expected phase advance on this time-frequency grid # - 2Ï€*y counts radians per second # - diff(x) counts seconds per frame # - The product counts radians per frame diff -= np.multiply.outer(2 * np.pi * y_coords, np.diff(x_coords, prepend=0.0)) # Wrap back to +-pi diff += np.pi np.mod(diff, 2 * np.pi, out=diff) diff -= np.pi # Use a cyclic colormap for the phase difference return diff, cmap_cyclic elif vscale == "dphase_t": # Same computation as above, but on the opposite axes diff = np.diff(np.unwrap(np.angle(data), axis=0), axis=0, prepend=0.0) diff -= np.multiply.outer(np.diff(y_coords, prepend=0.0), 2 * np.pi * x_coords) diff += np.pi np.mod(diff, 2 * np.pi, out=diff) diff -= np.pi return diff, cmap_cyclic else: # In some kind of dB mode _mode, scale_type, ref_ = __parse_vscale(vscale) if ref_ == "max": ref = np.max(np.abs(data)) elif ref_ is None: ref = 1.0 else: ref = float(ref_) if scale_type == "power": data = core.power_to_db(np.abs(data), top_db=top_db, ref=ref) else: data = core.amplitude_to_db(np.abs(data), top_db=top_db, ref=ref) # Use the default colormap for sequential data return data, cmap_seq VSCALE_PATTERN = re.compile( r"^(?PdBFS|dB)" # Match "dBFS" or "dB" r"(?:\[(?:(?Ppower)" # Optionally match [power r"(?:,(?P[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?))?" # Optional ref_power r"|(?P[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?))\])?$" # Or ref alone ) def __parse_vscale(vscale: str) -> tuple[str, str, float | str | None]: """Parse a vscale string into mode, scale_type, and reference value. Examples -------- - 'dBFS' -> ('dBFS', 'amplitude', 'max') - 'dBFS[power]' -> ('dBFS', 'power', 'max') - 'dB[power,0.1]' -> ('dB', 'power', 0.1) - 'dB[0.1]' -> ('dB', 'amplitude', 0.1) - 'dB' -> ('dB', 'amplitude', None) Parameters ---------- vscale : str Returns ------- mode is one of 'dBFS' or 'dB' scale_type is one of 'power' or 'amplitude' ref is a float, None, or 'max' """ match = VSCALE_PATTERN.fullmatch(vscale) if not match: raise ParameterError(f"Invalid vscale specification: {vscale}") mode = match.group("mode") scale_type = "power" if match.groupdict().get("type") else "amplitude" ref = match.groupdict().get("ref") or match.groupdict().get("ref_power") if mode == "dBFS": if ref is not None: raise ParameterError("dBFS vscale cannot have an explicit reference value") ref = "max" elif ref is not None: # mode == 'dB' ref = float(ref) return mode, scale_type, ref def waveshow( y: np.ndarray, *, sr: float = 22050, max_points: int = 11025, axis: str | None = "time", offset: float = 0.0, marker: str | MplPath | MarkerStyle = "", where: Literal["pre", "post", "mid"] = "post", label: str | None = None, transpose: bool = False, mask: ArrayLike | None = None, ax: mplaxes.Axes | None = None, invert: bool = False, invert_color: str | tuple | None = None, **kwargs: Any, ) -> AdaptiveWaveplot: """Visualize a waveform in the time domain. This function constructs a plot which adaptively switches between a raw samples-based view of the signal (`matplotlib.pyplot.step`) and an amplitude-envelope view of the signal (`matplotlib.pyplot.fill_between`) depending on the time extent of the plot's viewport. More specifically, when the plot spans a time interval of less than ``max_points / sr`` (by default, 1/2 second), the samples-based view is used, and otherwise a downsampled amplitude envelope is used. This is done to limit the complexity of the visual elements to guarantee an efficient, visually interpretable plot. When using interactive rendering (e.g., in a Jupyter notebook or IPython console), the plot will automatically update as the view-port is changed, either through widget controls or programmatic updates. .. note:: When visualizing stereo waveforms, the amplitude envelope will be generated so that the upper limits derive from the left channel, and the lower limits derive from the right channel, which can produce a vertically asymmetric plot. When zoomed in to the sample view, only the first channel will be shown. If you want to visualize both channels at the sample level, it is recommended to plot each signal independently. To visualize stereo waveforms as two separate signal displays, see `multiplot`. Parameters ---------- y : np.ndarray [shape=(n,) or (2,n)] audio time series (mono or stereo) If stereo, the left channel's amplitude envelope will be used for the top of the plot,, and the right channel's amplitude envelope (negated) will be used for the bottom of the plot. If mono, the signal's envelope is mirrored across the axis. sr : number > 0 [scalar] sampling rate of ``y`` (samples per second) max_points : int > 0 Maximum number of samples to draw. When the plot covers a time extent smaller than ``max_points / sr`` (default: 1/2 second), samples are drawn. If drawing raw samples would exceed `max_points`, then a downsampled amplitude envelope extracted from non-overlapping windows of `y` is visualized instead. The parameters of the amplitude envelope are defined so that the resulting plot cannot produce more than `max_points` frames. axis : str or None Display style of the axis ticks and tick markers. Accepted values are: - 'time' : markers are shown as milliseconds, seconds, minutes, or hours. Values are plotted in units of seconds. - 'h' : markers are shown as hours, minutes, and seconds. - 'm' : markers are shown as minutes and seconds. - 's' : markers are shown as seconds. - 'ms' : markers are shown as milliseconds. - 'lag' : like time, but past the halfway point counts as negative values. - 'lag_h' : same as lag, but in hours. - 'lag_m' : same as lag, but in minutes. - 'lag_s' : same as lag, but in seconds. - 'lag_ms' : same as lag, but in milliseconds. - `None`, 'none', or 'off': ticks and tick markers are hidden. offset : float Offset (in seconds) to start the waveform plot marker : str Marker symbol to use for sample values. (default: no markers) See Also: `matplotlib.markers`. where : {'pre', 'mid', 'post'} This setting determines how both waveform and envelope plots interpolate between observations. See `matplotlib.pyplot.step` for details. Default: 'post' label : str or None The label string applied to this plot. Note that the label transpose : bool If `True`, display the wave vertically instead of horizontally. mask : np.ndarray [shape=(n,)] or None If provided, this mask will be used to determine which samples to display. The mask should be a 1D boolean array of the same length as `y` (`y.shape[-1]`), where `True` indicates that the sample should be displayed, and `False` indicates that it should be ignored. .. note:: This mask is only used directly by the envelope display, and a raw sample display will not be masked. The `mask` parameter is intended to be used by the `wavef0` function, and it is not recommended to be used directly by the user. ax : matplotlib.axes.Axes or None Axes to plot on instead of the default `plt.gca()`. invert : bool If `True`, invert the foreground and background of the display, so that the axes background is colored. If `False` (default), the waveform display is colored and the background is unchanged. .. note:: This option should only be used if the wave display is the only element in the axes. invert_color : str, tuple, None If `invert` is `True`, this parameter specifies the color to use for the inverted waveform display. If `None` (default), the color is set to the current axes background color. **kwargs Additional keyword arguments to `matplotlib.pyplot.fill_between` and `matplotlib.pyplot.step`. Note that only those arguments which are common to both functions will be supported. Returns ------- librosa.display.AdaptiveWaveplot An object of type `librosa.display.AdaptiveWaveplot` See Also -------- wavebars AdaptiveWaveplot multiplot matplotlib.pyplot.step matplotlib.pyplot.fill_between matplotlib.pyplot.fill_betweenx matplotlib.markers Examples -------- Plot a monophonic waveform with an envelope view >>> import matplotlib.pyplot as plt >>> y, sr = librosa.loadx('choice', duration=10) >>> fig, ax = plt.subplots(nrows=3, sharex=True) >>> librosa.display.waveshow(y, sr=sr, ax=ax[0]) >>> ax[0].set(title='Envelope view, mono') >>> ax[0].label_outer() Or a stereo waveform >>> y, sr = librosa.loadx('choice', mono=False, duration=10) >>> librosa.display.waveshow(y, sr=sr, ax=ax[1]) >>> ax[1].set(title='Envelope view, stereo') >>> ax[1].label_outer() Or harmonic and percussive components with transparency >>> y, sr = librosa.loadx('choice', duration=10) >>> y_harm, y_perc = librosa.effects.hpss(y) >>> librosa.display.waveshow(y_harm, sr=sr, color='C1', alpha=0.75, ax=ax[2], label='Harmonic') >>> librosa.display.waveshow(y_perc, sr=sr, color='C2', alpha=0.75, ax=ax[2], label='Percussive') >>> ax[2].set(title='Multiple waveforms') >>> ax[2].legend() >>> plt.show() Zooming in on a plot to show raw sample values >>> fig, (ax, ax2) = plt.subplots(nrows=2, sharex=True) >>> ax.set(xlim=[6.1, 6.25], title='Sample view') >>> librosa.display.waveshow(y, sr=sr, ax=ax, label='Full signal') >>> librosa.display.waveshow(y_harm, sr=sr, color='C1', alpha=0.75, ax=ax2, label='Harmonic') >>> librosa.display.waveshow(y_perc, sr=sr, color='C2', alpha=0.75, ax=ax2, label='Percussive') >>> ax.label_outer() >>> ax.legend() >>> ax2.legend(ncols=2) >>> plt.show() Plotting a transposed wave along with a self-similarity matrix >>> fig, ax = plt.subplot_mosaic("hSSS;hSSS;hSSS;.vvv", layout='compressed') >>> y, sr = librosa.loadx('trumpet') >>> chroma = librosa.feature.chroma_cqt(y=y, sr=sr) >>> sim = librosa.segment.recurrence_matrix(chroma, mode='affinity') >>> librosa.display.specshow(sim, ax=ax['S'], sr=sr, ... x_axis='time', y_axis='time', ... auto_aspect=False) >>> ax['S'].label_outer() >>> ax['S'].sharex(ax['v']) >>> ax['S'].sharey(ax['h']) >>> ax['S'].set(title='Self-similarity') >>> librosa.display.waveshow(y, ax=ax['v']) >>> ax['v'].label_outer() >>> ax['v'].set(title='transpose=False') >>> librosa.display.waveshow(y, ax=ax['h'], transpose=True) >>> ax['h'].label_outer() >>> ax['h'].set(title='transpose=True') >>> plt.show() """ util.valid_audio(y) # Pad an extra channel dimension, if necessary if y.ndim == 1: y = y[np.newaxis, :] if max_points <= 0: raise ParameterError(f"max_points={max_points} must be strictly positive") # Create the adaptive drawing object axes = __check_axes(ax) # Reduce by envelope calculation # this choice of hop ensures that the envelope has at most max_points values hop_length = max(1, y.shape[-1] // max_points) y_env = __envelope(y, hop_length) # Split the envelope into top and bottom y_bottom, y_top = -y_env[-1], y_env[0] times = offset + core.times_like(y, sr=sr, hop_length=1) # Only plot up to max_points worth of data here xdata, ydata = times[:max_points], y[0, :max_points] dec_axis: matplotlib.axis.Axis if transpose: ydata, xdata = xdata, ydata filler = axes.fill_betweenx signal = "ylim_changed" dec_axis = axes.yaxis else: filler = axes.fill_between signal = "xlim_changed" dec_axis = axes.xaxis if mask is not None: mask = cast( "Sequence[bool]", np.asarray(mask, dtype=bool)[: len(y_top) * hop_length : hop_length] ) (steps,) = axes.step(xdata, ydata, marker=marker, where=where, **kwargs) # Pull color property from the steps object, if we don't already have it if "color" not in kwargs: kwargs.setdefault("color", steps.get_color()) envelope = filler( times[: len(y_top) * hop_length : hop_length], y_bottom, y_top, step=where, where=mask, **kwargs, ) adaptor = AdaptiveWaveplot( times, y[0], steps, envelope, sr=sr, max_samples=max_points, transpose=transpose, label=label, ) # Register adaptor to keep it alive as long as Axes exists bucket = _WAVESHOW_ADAPTORS.get(axes) if bucket is None: bucket = set() _WAVESHOW_ADAPTORS[axes] = bucket bucket.add(adaptor) adaptor.connect(axes, signal=signal) # Force an initial update to ensure the state is consistent adaptor.update(axes) # Handle color inversion if needed if invert: # If no inverted color is given, just swap it from the axes face if invert_color is None: invert_color = axes.patch.get_facecolor() # Get the fg color from the steps plot color = steps.get_color() # Set the axes facecolor to our wave color axes.patch.set_facecolor(color) steps.set_color(invert_color) envelope.set_color(invert_color) # Construct tickers and locators __decorate_axis(dec_axis, axis) return adaptor def wavebars( y: np.ndarray, *, sr: float = 22050, n_bars: int = 100, gap_ratio: float = 0.4, rounding_ratio: float = 0.5, axis: str | None = "time", offset: float = 0.0, invert: bool = False, invert_color: str | tuple | None = None, transpose: bool = False, label: str | None = None, ax: mplaxes.Axes | None = None, **patch_kwargs: Any, ) -> mcollections.PatchCollection: """Visualize a waveform as a series of bars representing the amplitude envelope. This visualization is appropriate for displaying a simplified view of the signal, and is best suited for small figures where simplicity is desired. Parameters ---------- y : np.ndarray [shape=(n,) or (2,n)] audio time series (mono or stereo) If stereo, the left channel's amplitude envelope will be used for the top of the bars, and the right channel's amplitude envelope (negated) will be used for the bottom of the bars. If mono, the signal's envelope is mirrored across the axis. sr : number > 0 [scalar] sampling rate of ``y`` (samples per second) n_bars : int > 0 Number of bars to display in the waveform plot. The total time extent of the plot will be divided into `n_bars` segments, and the amplitude envelope of each segment will be represented as a bar. gap_ratio : float in [0, 1] The fraction of the bar width that will be left as a gap between adjacent bars. rounding_ratio : float in [0, 1] The fraction of the bar width that will be used for rounding the corners of the bars. A value of 0.5 will produce bars with rounded corners, while a value of 0 will produce rectangular bars. axis : str or None Display style of the axis ticks and tick markers. Accepted values are: - 'time' : markers are shown as milliseconds, seconds, minutes, or hours. - 'h' : markers are shown as hours, minutes, and seconds. - 'm' : markers are shown as minutes and seconds. - 's' : markers are shown as seconds. - 'ms' : markers are shown as milliseconds. - 'lag' : like time, but past the halfway point counts as negative values. - 'lag_h' : same as lag, but in hours. - 'lag_m' : same as lag, but in minutes. - 'lag_s' : same as lag, but in seconds. - 'lag_ms' : same as lag, but in milliseconds. - `None`, 'none', or 'off': ticks and tick markers are hidden. offset : float Offset (in seconds) to start the waveform plot. invert : bool If `True`, invert the foreground and background of the display, so that the axes background is colored. If `False` (default), the envelope display is colored and the background is unchanged. invert_color : str, tuple, None If `invert` is `True`, this parameter specifies the color to use for the inverted waveform display. If `None` (default), the color is set to the current axes background color. transpose : bool If `True`, display the wave vertically instead of horizontally. label : str or None The label string applied to this plot. If `None`, no label is applied. ax : matplotlib.axes.Axes or None Axes to plot on instead of the default `plt.gca()`. **patch_kwargs : dict Additional keyword arguments to pass to `matplotlib.patches.FancyBboxPatch` Returns ------- matplotlib.collections.PatchCollection A collection of patches representing the amplitude envelope of the waveform. See Also -------- waveshow Examples -------- Plot a waveform as bars, compared to the `waveshow` version of the plot >>> import matplotlib.pyplot as plt >>> y, sr = librosa.loadx('libri1', duration=10) >>> fig, ax = plt.subplots(nrows=2, sharex=True) >>> librosa.display.waveshow(y=y, sr=sr, ax=ax[0], label='waveshow()') >>> ax[0].legend() >>> ax[0].label_outer() >>> librosa.display.wavebars(y=y, sr=sr, ax=ax[1], label='wavebars()') >>> ax[1].legend() >>> plt.show() Make plots with varying amounts of detail, squared corners, and inverted colors. >>> fig, ax = plt.subplots(nrows=3, sharex=True, sharey=True) >>> librosa.display.wavebars(y=y, sr=sr, n_bars=100, rounding_ratio=0, ... invert=True, ax=ax[0], label='100 bars') >>> librosa.display.wavebars(y=y, sr=sr, n_bars=200, rounding_ratio=0, ... color='C1', invert=True, ax=ax[1], label='200 bars') >>> librosa.display.wavebars(y=y, sr=sr, n_bars=50, rounding_ratio=0, ... color='C2', invert=True, ax=ax[2], label='50 bars') >>> ax[0].legend() >>> ax[1].legend() >>> ax[2].legend() >>> ax[0].label_outer() >>> ax[1].label_outer() >>> plt.show() """ util.valid_audio(y) if y.ndim == 1: y = y[np.newaxis, :] patch_kwargs.setdefault("linewidth", 0) axes = __check_axes(ax) hop = max(1, y.shape[-1] // n_bars) env = __envelope(y, hop) env_bottom, env_top = env[-1], env[0] bar_width = (hop / sr) * (1 - gap_ratio) rounding_size = bar_width * rounding_ratio times = offset + core.times_like(env, sr=sr, hop_length=hop) patches = [] boxstyle = f"round,pad=0,rounding_size={rounding_size}" for t, a0, a1 in zip(times, env_bottom, env_top, strict=True): base = min(-rounding_size, -a0) top = max(rounding_size, a1) if transpose: xy, width, height = (base, t), top - base, bar_width else: xy, width, height = (t, base), bar_width, top - base p = mpatches.FancyBboxPatch( xy, width, height, boxstyle=boxstyle, ) patches.append(p) patch_kwargs.setdefault("transform", axes.transData) coll = mcollections.PatchCollection(patches, **patch_kwargs) axes.add_collection(coll) # Create a proxy artist if we have a label to set # Even if we don't have a label, we'll still need it for handling inversion later on proxy = mpatches.FancyBboxPatch( (np.nan, np.nan), 1, 1, boxstyle=boxstyle, label=label, **patch_kwargs ) proxy.set_in_layout(False) if label is not None: axes.add_patch(proxy) axes.autoscale_view() if invert: # If no inverted color is given, just swap it from the axes face if invert_color is None: invert_color = axes.patch.get_facecolor() # Get the fg color from the steps plot color = coll.get_facecolor() # Set the axes facecolor to our wave color axes.patch.set_facecolor(color) # type: ignore[arg-type] proxy.set_facecolor(color) # type: ignore[arg-type] coll.set_facecolor(invert_color) if transpose: __decorate_axis(axes.yaxis, axis) else: __decorate_axis(axes.xaxis, axis) return coll def wavef0( y: np.ndarray, *, f0: np.ndarray, sr: float = 22050, hop_length: int = 512, bins_per_octave: int = 12, time_axis: str = "time", freq_axis: str = "cqt_note", offset: float = 0.0, key: str = "C:maj", Sa: float | None = None, mela: str | int | None = None, thaat: str | None = None, unicode: bool = True, ax: mplaxes.Axes | None = None, method: str = "waveshow", transpose: bool = False, **kwargs: Any, ) -> AdaptiveWaveplot | mcollections.PatchCollection: """Visualize a waveform with an f0-displacement. This can be used to simultaneously visualize the fundamental frequency (f0) estimates and the waveform or amplitude envelope of an audio signal in one compact display. Parameters ---------- y : np.ndarray [shape=(n,) or (2,n)] audio time series (mono or stereo) If stereo, the left channel's amplitude envelope will be used for the top of the plot, and the right channel's amplitude envelope (negated) will be used for the bottom of the plot. If mono, the signal's envelope is mirrored across the axis. f0 : np.ndarray [shape=(m,)] Fundamental frequency (f0) estimates in Hz. This should be computed using a pitch estimation algorithm such as `librosa.pyin` or `librosa.yin`. sr : number > 0 [scalar] sampling rate of ``y`` (samples per second) hop_length : int > 0 Hop length (in samples) between successive f0 estimates. This should match the hop length used to compute `f0`. bins_per_octave : int > 0 Number of frequency bins per octave used to scale the waveform's amplitude displacement around f0. Combined with the waveform's peak amplitude (used as the displacement norm), this controls how many bins of vertical displacement correspond to one octave above or below f0. time_axis : str Display style of the time axis ticks and tick markers. Accepted values are: - 'time' : markers are shown as milliseconds, seconds, minutes, or hours. - 'h' : markers are shown as hours, minutes, and seconds. - 'm' : markers are shown as minutes and seconds. - 's' : markers are shown as seconds. - 'ms' : markers are shown as milliseconds. - 'lag' : like time, but past the halfway point counts as negative values. - 'lag_h' : same as lag, but in hours. - 'lag_m' : same as lag, but in minutes. - 'lag_s' : same as lag, but in seconds. - 'lag_ms' : same as lag, but in milliseconds. - `None`, 'none', or 'off': ticks and tick markers are hidden. freq_axis : str Display style of the frequency axis ticks and tick markers. Accepted values are: - 'cqt_note' : markers are shown as note names. - 'cqt_hz' : markers are shown as frequencies in Hz. - 'cqt_oct3' : markers are shown in Hz using 1/3-octave intervals. - 'cqt_svara' : markers are shown as Indian classical music svara names. offset : float Offset (in seconds) to start the waveform plot. key : str Key signature for the frequency axis. This is used to determine the note names for the frequency axis when using `cqt_note` mode. Sa : float or None Sa (tonic) frequency in Hz for the frequency axis. Required for `cqt_svara` mode. mela : str or int or None Mela (scale) name or index for the frequency axis. This is used to determine the svara names for the frequency axis when using `cqt_svara` mode. thaat : str or None Thaat (scale) name for the frequency axis. This is used to determine the svara names for the frequency axis when using `cqt_svara` mode. unicode : bool If `True`, use Unicode characters for frequency axis labels. ax : matplotlib.axes.Axes or None Axes to plot on instead of the default `plt.gca()`. method : str Method to use for visualizing the waveform with f0 displacement. Accepted values are: - 'waveshow' : Use `librosa.display.waveshow` to visualize the waveform with an f0 displacement. - 'wavebars' : Use `librosa.display.wavebars` to visualize the waveform as bars with an f0 displacement. transpose : bool If `True`, display the wave vertically instead of horizontally. **kwargs : dict Additional keyword arguments forwarded to the plotting function selected by `method`. If `method='waveshow'`, these must be keyword arguments supported by `librosa.display.waveshow` (for example, `max_points`). If `method='wavebars'`, these must be keyword arguments supported by `librosa.display.wavebars` (for example, `n_bars`, `gap_ratio`, `rounding_ratio`, `invert`, and `invert_color`). Keyword arguments for one method are not valid when using the other. Returns ------- AdaptiveWaveplot or PatchCollection An object of type `librosa.display.AdaptiveWaveplot` if `method='waveshow'`, or a `matplotlib.collections.PatchCollection` if `method='wavebars'`. See Also -------- waveshow wavebars Examples -------- Visualize a waveform with an f0 displacement using `waveshow` >>> import matplotlib.pyplot as plt >>> y, sr = librosa.loadx('trumpet') >>> f0, _, _ = librosa.pyin(y, fmin=librosa.note_to_hz('C2'), ... fmax=librosa.note_to_hz('C7'), ... sr=sr, hop_length=512) >>> fig, ax = plt.subplots() >>> librosa.display.wavef0(y=y, f0=f0, sr=sr, ax=ax, ... method='waveshow') >>> ax.set(title='Waveform with f0 displacement (waveshow)') >>> plt.show() Visualize a waveform with an f0 displacement using `wavebars`, and Hz labels instead of note names. Using a larger number of bars shows more detail here. >>> fig, ax = plt.subplots() >>> librosa.display.wavef0(y=y, f0=f0, sr=sr, ax=ax, n_bars=256, ... method='wavebars', freq_axis='cqt_hz') >>> ax.set(title='Waveform with f0 displacement (wavebars, cqt_hz)') >>> plt.show() Overlay a displaced waveform on a CQT plot via `specshow`. >>> fig, ax = plt.subplots() >>> C = librosa.cqt(y, sr=sr) >>> librosa.display.specshow(C, ax=ax, sr=sr, x_axis='time', y_axis='cqt_note', ... vscale='dBFS', cmap='gray_r') >>> hl = librosa.display.highlight(ax=ax) >>> librosa.display.wavef0(y=y, f0=f0, sr=sr, ax=ax, path_effects=hl) """ # Create the adaptive drawing object axes = __check_axes(ax) if method not in ("waveshow", "wavebars"): raise ParameterError(f"Invalid display method={method}.") # Force norm to be strictly positive and handle empty arrays norm = float(util.tiny(y)) if y.size > 0: norm += max(y.max(), -y.min()) trans = Transformf0( f0, sr=sr, hop_length=hop_length, bins_per_octave=bins_per_octave, norm=norm, offset=offset, transpose=transpose, ) # and transposed mode here if transpose: __decorate_axis( axes.xaxis, freq_axis, key=key, Sa=Sa, mela=mela, thaat=thaat, unicode=unicode, ) else: __decorate_axis( axes.yaxis, freq_axis, key=key, Sa=Sa, mela=mela, thaat=thaat, unicode=unicode, ) if method == "waveshow": times = offset + np.arange(y.shape[-1]) / sr mask = np.isfinite(trans.f0_interp(times)) adaptor = waveshow( y=y, sr=sr, axis=time_axis, offset=offset, mask=mask, ax=axes, transform=trans + axes.transData, transpose=transpose, **kwargs, ) # Kludge the data limits because the fill_between collection does not automatically # update the data limits assert adaptor.envelope is not None xy = adaptor.envelope.get_datalim(trans + axes.transData).get_points() f0min = np.nanmin(f0) f0max = np.nanmax(f0) if transpose: handle = mlines.Line2D([xy[0, 0] + f0min, xy[1, 0] + f0max], xy[:, 1]) else: handle = mlines.Line2D(xy[:, 0], [xy[0, 1] + f0min, xy[1, 1] + f0max]) axes.add_line(handle) axes.autoscale_view() handle.remove() # end kludge return adaptor else: return wavebars( y=y, sr=sr, axis=time_axis, offset=offset, ax=axes, transform=trans + axes.transData, transpose=transpose, **kwargs, ) def __radian_formatter(x, pos): """Format a tick value (in radians) as a rational multiple of pi""" m = x / np.pi # hard to imagine going finer than pi/16 (11°) frac = Fraction(m).limit_denominator(16) num, den = frac.numerator, frac.denominator if num == 0: return " 0" sign = "-" if num * den < 0 else " " num_abs = abs(num) # Build numerator string coeff = "" if num_abs == 1 else str(num_abs) if den == 1: return f"{sign}{coeff}Ï€" else: return f"{sign}{coeff}Ï€/{den}" def colorbar_phase( im: matplotlib.cm.ScalarMappable, *, numticks: int = 9, ax: matplotlib.axes.Axes | None = None, fig: matplotlib.figure.FigureBase | None = None, **kwargs: Any, ) -> matplotlib.colorbar.Colorbar: """Attach a colorbar to an image representing phase data in radians. The colorbar will display ticks at rational multiples of Ï€. Parameters ---------- im : matplotlib.cm.ScalarMappable The image to which the colorbar will be attached. Generally this will be a `matplotlib.image.AxesImage` or `matplotlib.collections.QuadMesh` as returned by `specshow`. numticks : int > 0 The number of ticks to display on the colorbar. Default is 9, corresponding to multiples of Ï€/4. ax : matplotlib.axes.Axes or None The axes to which the colorbar will be attached. If None, the colorbar will be attached to the axes of `im`. fig : matplotlib.figure.Figure, SubFigure, or None The figure to which the colorbar will be attached. If None, the colorbar will be attached to the figure of `im`. **kwargs Additional keyword arguments to pass to `fig.colorbar`. Returns ------- cbar : matplotlib.colorbar.Colorbar The created colorbar object. See Also -------- specshow colorbar_db matplotlib.colorbar.Colorbar Examples -------- Attach a colorbar to a phase spectrogram >>> import matplotlib.pyplot as plt >>> import librosa >>> y, sr = librosa.loadx('trumpet') >>> S = librosa.stft(y) >>> fig, ax = plt.subplots() >>> im = librosa.display.specshow(S, ax=ax, y_axis='log', x_axis='time', vscale='phase') >>> librosa.display.colorbar_phase(im) >>> plt.show() Attach a colorbar to one subplot axes, and show as multiples of Ï€/3. >>> fig, ax = plt.subplots(nrows=2, sharex=True, sharey=True) >>> im_mag = librosa.display.specshow(S, ax=ax[0], y_axis='log', x_axis='time', vscale='dBFS') >>> cbar = librosa.display.colorbar_db(im_mag, ax=ax[0], label='dBFS') >>> im_ph = librosa.display.specshow(S, ax=ax[1], y_axis='log', x_axis='time', vscale='dphase') >>> cbar = librosa.display.colorbar_phase(im_ph, ax=ax[1], numticks=7) >>> ax[0].label_outer() >>> plt.show() """ if fig is None: fig = im.figure if ax is None: ax = im.axes kwargs.setdefault("label", "radians") kwargs.setdefault("ticks", mplticker.LinearLocator(numticks=numticks)) kwargs.setdefault("format", mplticker.FuncFormatter(__radian_formatter)) cbar = fig.colorbar( im, ax=ax, **kwargs, ) return cbar def colorbar_db( im: matplotlib.cm.ScalarMappable, *, ax: matplotlib.axes.Axes | None = None, fig: matplotlib.figure.FigureBase | None = None, format: str | mplticker.Formatter = "% -3.f", **kwargs: Any, ) -> matplotlib.colorbar.Colorbar: """Attach a colorbar to an image representing decibel-scaled data. Parameters ---------- im : matplotlib.cm.ScalarMappable The image to which the colorbar will be attached. Generally this will be a `matplotlib.image.AxesImage` or `matplotlib.collections.QuadMesh` as returned by `specshow`. ax : matplotlib.axes.Axes or None The axes to which the colorbar will be attached. If None, the colorbar will be attached to the axes of `im`. fig : matplotlib.figure.Figure, SubFigure, or None The figure to which the colorbar will be attached. If None, the colorbar will be attached to the figure of `im`. format : str The format string for the colorbar ticks. Default is "% -3.f", which displays integer values. You can change this to a different format if needed. **kwargs Additional keyword arguments to pass to `fig.colorbar`. Returns ------- cbar : matplotlib.colorbar.Colorbar The created colorbar object. See Also -------- specshow colorbar_phase matplotlib.colorbar.Colorbar Examples -------- Attach a colorbar to a magnitude spectrogram >>> import matplotlib.pyplot as plt >>> import librosa >>> y, sr = librosa.loadx('trumpet') >>> S = librosa.stft(y) >>> fig, ax = plt.subplots() >>> im = librosa.display.specshow(S, ax=ax, y_axis='log', x_axis='time', vscale='dB') >>> librosa.display.colorbar_db(im) >>> plt.show() Attach a colorbar to one subplot axes. We can also set a label for the colorbar. >>> fig, ax = plt.subplots(nrows=2, sharex=True, sharey=True) >>> im_mag = librosa.display.specshow(S, ax=ax[0], y_axis='log', x_axis='time', vscale='dBFS') >>> cbar = librosa.display.colorbar_db(im_mag, ax=ax[0], label='dBFS') >>> im_ph = librosa.display.specshow(S, ax=ax[1], y_axis='log', x_axis='time', vscale='dphase') >>> cbar = librosa.display.colorbar_phase(im_ph, ax=ax[1]) >>> ax[0].label_outer() >>> plt.show() """ if fig is None: fig = im.figure if ax is None: ax = im.axes kwargs.setdefault("label", "dB") cbar = fig.colorbar( im, ax=ax, format=format, **kwargs, ) return cbar def _squeeze_shape(shape: tuple[int, ...]) -> tuple[int, ...]: """Check if two shape arrays are equivalent after squeezing out singleton dimensions.""" return tuple(dim for dim in shape if dim > 1) def _resolve_multiplot( func: Literal["waveshow", "wavebars", "specshow"], ) -> tuple[Callable[..., Any], int, list[str]]: """Resolve multiplot function names. Parameters ---------- func : str The name of the display function to use for the multiplot. Accepted values are 'waveshow', 'wavebars', and 'specshow'. Returns ------- function : callable The display function corresponding to the given name. dims : int The number of data dimensions that each call to the display function expects. badprops : list of str A list of property names that are not supported by the display function and should be removed from the style cycle when sharing properties. """ display_map: dict[str, tuple[Callable[..., Any], int, list[str]]] = { "waveshow": (waveshow, 1, []), "wavebars": (wavebars, 1, []), "specshow": (specshow, 2, ["color"]), } try: return display_map[func] except KeyError as exc: raise ParameterError(f"Invalid display '{func}' for multiplot") from exc def _mp_get_layout( data: tuple[np.ndarray, ...], dims: int, orient: Literal["h", "v"] ) -> tuple[tuple[int, ...], int, int, bool]: """Determine the layout of a multiplot grid based on the data shape and orientation. Parameters ---------- data : tuple of ndarray The input data for the multiplot. The shape of this data will determine the layout of the grid. dims : int The number of data dimensions that each call to the display function expects. orient : str {'h', 'v'} The orientation of the multiplot grid. Accepted values are 'h' for horizontal and 'v' for vertical. Returns ------- axshape : tuple of int The shape of the grid of axes, determined by the shape of the input data and the specified orientation. nrows : int The number of rows in the grid of axes. ncols : int The number of columns in the grid of axes. multi_input : bool If the input contains multiple separate arrays to plot, this flag is True. Otherwise, False. """ if orient not in ("h", "v"): raise ParameterError(f"Invalid value orient={orient}") multi_plot = False if len(data) == 1 and isinstance(data[0], np.ndarray) and data[0].ndim > dims: data_stack = np.asarray(data[0]) axshape = data_stack.shape[:-dims] elif len(data) >= 1: multi_plot = True axshape = (len(data),) else: raise ParameterError("multiplot requires at least one data array to plot") if len(axshape) == 1: if orient == "v": nrows, ncols = axshape[0], 1 else: nrows, ncols = 1, axshape[0] elif len(axshape) == 2: # Yes this is awkward, but it makes the type checker work. # In a sane world it would just be nrows, ncols = axshape nrows, ncols = axshape[0], axshape[-1] else: raise ParameterError(f"Invalid axes shape={axshape}") return axshape, nrows, ncols, multi_plot def _mp_setup_axes( *, axes: matplotlib.axes.Axes | np.ndarray | None, fig: matplotlib.figure.FigureBase | None = None, fig_kw: dict | None = None, nrows: int, ncols: int, axshape: tuple[int, ...], orient: Literal["h", "v"], sharex: bool, sharey: bool, ) -> tuple[matplotlib.figure.FigureBase, npt.NDArray[np.object_], tuple[int, ...]]: """Set up the figure and axes for a multiplot grid. Parameters ---------- axes : matplotlib.axes.Axes, np.ndarray, or None The axes to use for the multiplot. If None, a new figure and axes will be created. If a single Axes object is provided, it will be used for all subplots. If an array of Axes objects is provided, it must be compatible with the shape of the data. fig : matplotlib.figure.FigureBase or None The figure to use for the multiplot. If None, a new figure will be created if needed. fig_kw : dict or None Additional keyword arguments to pass to `plt.subplots` when creating a new figure. nrows : int The number of rows in the grid of axes. ncols : int The number of columns in the grid of axes. axshape : tuple of int The shape of the grid of axes, determined by the shape of the input data and the specified orientation. orient : str {'h', 'v'} The orientation of the multiplot grid. Accepted values are 'h' for horizontal and 'v' for vertical. sharex : bool Whether to share the x-axis among subplots when creating a new figure. sharey : bool Whether to share the y-axis among subplots when creating a new figure. Returns ------- fig : matplotlib.figure.FigureBase The figure object for the multiplot. axes : np.ndarray An array of Axes objects for the multiplot, with shape compatible with the input data. output_shape : tuple of int The shape of the output array of display objects, determined by the shape of the axes. """ output_shape = axshape if axes is None: if fig is None: if fig_kw is None: fig_kw = {} fig, axes = plt.subplots( nrows=nrows, ncols=ncols, sharex=sharex, sharey=sharey, squeeze=False, **fig_kw, ) else: axes = fig.subplots( nrows=nrows, ncols=ncols, sharex=sharex, sharey=sharey, squeeze=False, ) elif isinstance(axes, np.ndarray): output_shape = axes.shape if axes.ndim == 1: if orient == "v": axes = axes[:, np.newaxis] else: axes = axes[np.newaxis, :] else: if not isinstance(axes, np.ndarray): output_shape = tuple() axes = np.atleast_2d(np.asarray(axes)) # Ensure that axes object is now encapsulated in numpy arrays axes = np.asarray(axes, dtype=object) # Populate fig with the figure from the axes object. fig = axes.flat[0].get_figure() if _squeeze_shape(axes.shape) != _squeeze_shape(axshape): raise ParameterError(f"axes shape={axes.shape} is incompatible with data shape") return fig, axes, output_shape def _mp_setup_labels( labels: Sequence[str | None] | None, shape: tuple[int, ...] ) -> npt.NDArray[np.object_]: """Set up the labels for a multiplot grid. Parameters ---------- labels : sequence of str or None The labels to apply to each subplot in the multiplot grid. If None, no labels will be applied. If a sequence is provided, it must be compatible with the shape of the axes. shape : tuple of int The shape of the grid of axes, determined by the shape of the input data and the specified orientation. Returns ------- np.ndarray An array of labels for each subplot in the multiplot grid, with shape compatible with the axes. """ if labels is None: return np.full(shape, None, dtype=object) return np.asarray(labels, dtype=object).reshape(shape) def _mp_setup_prop_group( share_properties: bool | Literal["row", "col"] | ArrayLike | None, shape: tuple[int, ...], ) -> np.ndarray: """Set up the property groups for a multiplot grid. This is used to determine how style properties (color, line style, etc.) are shared among different subplots in the grid. Parameters ---------- share_properties : bool, str, sequence, or None The property sharing scheme for the multiplot grid. Accepted values are: - `None` or `False`: no properties are shared, and each subplot is treated as a unique group. - `True`: all subplots share the same properties and belong to a single group. - 'row': subplots in the same row share properties and belong to the same group. - 'col': subplots in the same column share properties and belong to the same group. - sequence: a sequence of group identifiers for each subplot. The length of the sequence must match the total number of subplots (i.e., the product of the shape of the axes). shape : tuple of int The shape of the grid of axes, determined by the shape of the input data and the specified orientation. Returns ------- np.ndarray An array of group identifiers for each subplot in the multiplot grid, with shape compatible with the axes. """ if share_properties is None or share_properties is False: return np.arange(np.prod(shape)).reshape(shape) if share_properties is True: return np.ones(shape, dtype=int) if isinstance(share_properties, str) and share_properties == "row": return np.asarray(np.indices(shape)[0]) if isinstance(share_properties, str) and share_properties == "col": return np.asarray(np.indices(shape)[-1]) prop_group = np.asarray(share_properties) if prop_group.size != np.prod(shape): raise ParameterError( f"Shape mismatch between axes={shape} " f"and share_properties={prop_group.shape}" ) return prop_group.reshape(shape) def _mp_setup_properties( prop_group: np.ndarray, badprops: list[str], prop_cycle: cycler.Cycler | None ) -> npt.NDArray[np.object_]: """Set up the properties for each subplot in a multiplot grid based on the property groups. Parameters ---------- prop_group : np.ndarray An array of group identifiers for each subplot in the multiplot grid, with shape compatible with the axes. badprops : list of str A list of property names that are not supported by the display function and should be removed from the style cycle when sharing properties. prop_cycle : cycler.Cycler or None The property cycle to use for assigning properties to the subplots. If None, the default property cycle from `plt.rcParams["axes.prop_cycle"]` will be used. Returns ------- np.ndarray An array of property dictionaries for each subplot in the multiplot grid, with shape compatible with the axes. """ properties = np.empty(prop_group.shape, dtype=object) properties.fill(None) if prop_cycle is None: prop_cycle = plt.rcParams["axes.prop_cycle"] style_cycle = cycle(prop_cycle) style_map = {} for idx in np.ndindex(prop_group.shape): group = prop_group[idx] if group not in style_map: style = copy.deepcopy(next(style_cycle)) for prop in badprops: style.pop(prop, None) style_map[group] = style properties[idx] = style_map[group] return properties def multiplot( func: Literal["waveshow", "wavebars", "specshow"], *data: np.ndarray, axes: matplotlib.axes.Axes | np.ndarray | None = None, fig: matplotlib.figure.FigureBase | None = None, orient: Literal["v", "h"] = "v", share_properties: bool | Literal["row", "col"] | np.ndarray | None = None, fig_kw: dict | None = None, sharex: bool = True, sharey: bool = True, label_outer: bool = True, labels: Sequence[str | None] | None = None, titles: Sequence[str | None] | None = None, prop_cycle: cycler.Cycler | None = None, **kwargs: Any, ) -> npt.NDArray[np.object_]: """Visualize multiple related waveforms or spectrograms on an array of subplots. Example use cases include: - Displaying multiple waveforms from a multi-channel audio file. - Displaying multiple spectrograms from a multi-channel audio file. Parameters ---------- func : str The name of the display function to use for the multiplot. Accepted values are 'waveshow', 'wavebars', and 'specshow'. *data : one or more `np.ndarray` The input data for the multiplot. If one array is provided, it is interpreted as a multi-channel array, where the leading dimensions correspond to different channels or signals to plot. If multiple arrays are provided, each array is treated as a single channel or input signal, and visualized on its own subplot. axes : matplotlib.axes.Axes, np.ndarray, or None The axes to use for the multiplot. If None, a new axes array will be created on `fig`. If an array of Axes objects is provided, it must be compatible with the shape of the data. If a single axes object is provided, it will be interpreted as a 1x1 array (i.e. a single subplot). fig : matplotlib.figure.FigureBase or None The figure to use for the multiplot. If None, a new figure will be created if needed. If `axes` is provided, the figure will be inferred from `axes` and the `fig` parameter will be ignored. orient : str {'h', 'v'} The orientation of the multiplot grid. Accepted values are 'h' for horizontal and 'v' for vertical. This determines how the subplots are arranged when the input data has a single non-singleton dimension (e.g., shape (n, k) with k > 1). share_properties : bool, str, np.ndarray, or None The property sharing scheme for the multiplot grid. Accepted values are: - `None` or `False`: no properties are shared, and each subplot is treated as a unique group. - `True`: all subplots share the same properties and belong to a single group. - 'row': subplots in the same row share properties and belong to the same group - 'col': subplots in the same column share properties and belong to the same group. - np.ndarray: a custom array of group identifiers for each subplot. The shape of the array must match the shape of the axes grid. Any two elements with the same value are considered to be in the same group and will share properties. fig_kw : dict or None Additional keyword arguments to pass to `plt.subplots` when creating a new figure. sharex : bool Whether to share the x-axis among subplots when creating a new figure. sharey : bool Whether to share the y-axis among subplots when creating a new figure. label_outer : bool Whether to only show labels on the outer axes when using shared axes. labels : sequence of str or None The labels to apply to each subplot in the multiplot grid. If None, no labels will be applied. If a sequence is provided, it must be compatible with the shape of the axes. titles : sequence of str or None The titles to apply to each subplot in the multiplot grid. If None, no titles will be applied. If a sequence is provided, it must be compatible with the shape of the axes. prop_cycle : cycler.Cycler or None The property cycle to use for assigning properties to the subplots. If None, the default property cycle from `plt.rcParams["axes.prop_cycle"]` will be used. **kwargs Additional keyword arguments to pass to the display function for each subplot. Returns ------- np.ndarray An array of display objects returned by the display function for each subplot in the multiplot grid The shape of this array will be compatible with the shape of the axes grid. See Also -------- waveshow wavebars specshow legend_for_axes Examples -------- Display multiple synchronized signals stacked in an array. We'll let multiplot create the figure and axes objects for us. >>> import matplotlib.pyplot as plt >>> y, sr = librosa.loadx('choice', duration=10) >>> yh, yp = librosa.effects.hpss(y) >>> librosa.display.multiplot('waveshow', y, yh, yp, ... labels=['Original', 'Harmonic', 'Percussive'], ... # The remaining parameters are passed through to waveshow ... sr=sr, ... invert=True) >>> plt.gcf().legend() >>> plt.show() Multiplot can also accept preconstructed axes as input, provided that they are compatible with the shape of the data. The below example does this with a spectrogram display. >>> y_stack = librosa.to_multi(y, yh, yp) >>> stft = librosa.stft(y=y_stack) >>> fig, ax = plt.subplots(nrows=3, sharex=True, sharey=True, figsize=(8, 8)) >>> img = librosa.display.multiplot('specshow', stft, axes=ax, ... titles=['Original', 'Harmonic', 'Percussive'], ... x_axis='time', y_axis='log', vscale='dBFS') >>> librosa.display.colorbar_db(img[0], ax=ax, label='dBFS') >>> plt.show() """ # Identify the display function and the expected data dimensions for each subplot function, dims, badprops = _resolve_multiplot(func) # Determine the layout of the multiplot grid based on the data shape and orientation axshape, nrows, ncols, multi_input = _mp_get_layout(data, dims, orient) # Set up the figure and axes for the multiplot grid fig, axes, output_shape = _mp_setup_axes( axes=axes, fig=fig, fig_kw=fig_kw, nrows=nrows, ncols=ncols, axshape=axshape, orient=orient, sharex=sharex, sharey=sharey, ) # Set up the labels and properties for each subplot in the multiplot grid labels = _mp_setup_labels(labels, axes.shape) titles = _mp_setup_labels(titles, axes.shape) prop_group = _mp_setup_prop_group(share_properties, axes.shape) properties: np.ndarray = _mp_setup_properties(prop_group, badprops, prop_cycle) # Allocate the output array output = np.empty_like(axes, dtype=object) # Iterate over each subplot and call the display function with the appropriate data, axes, labels, and properties for idx in np.ndindex(axshape): flat_idx = np.ravel_multi_index(idx, axshape) if multi_input: # User provided variadic inputs, so use flat indexing datum = data[flat_idx] else: # User already stacked the inputs into one array. datum = data[0][idx] output.flat[flat_idx] = function( datum, ax=axes.flat[flat_idx], label=labels.flat[flat_idx], **properties.flat[flat_idx], **kwargs, ) if titles.flat[flat_idx] is not None: axes.flat[flat_idx].set_title(titles.flat[flat_idx]) if label_outer: axes.flat[flat_idx].label_outer() # Reshape the output array to match the shape of the axes grid return output.reshape(output_shape) def legend_for_axes( axes: matplotlib.axes.Axes | np.ndarray | list[matplotlib.axes.Axes] | None = None, *, fig: matplotlib.figure.Figure | None = None, **kwargs: Any, ) -> matplotlib.legend.Legend: """Create a figure-level legend for a collection of axes. This is similar to `matplotlib.figure.Figure.legend`, but it limits the handle collection to only those belonging to the specified axes. This makes it easier to create different legends for subsets of a subplot array. Parameters ---------- axes : matplotlib.axes.Axes or array-like of Axes, optional Axes to include in the legend aggregation. If not provided, axes are taken from `fig.axes`, or from the current figure if `fig` is not provided. fig : matplotlib.figure.Figure, optional Figure on which to create the legend. If not provided, it is inferred from `axes`, or from `plt.gcf()` if `axes` is also not provided. **kwargs Additional keyword arguments passed to `matplotlib.figure.Figure.legend`. Returns ------- legend : matplotlib.legend.Legend The created legend. Examples -------- If no axes are provided, we aggregate legends across all subplots on the current figure: >>> import matplotlib.pyplot as plt >>> x = np.linspace(-10, 10, 100) >>> fig, ax = plt.subplots(nrows=2, ncols=1, sharex=True) >>> ax[0].plot(x, label='Line', color='C0') >>> ax[1].plot(x**2, label='Parabola', color='C1') >>> librosa.display.legend_for_axes() >>> plt.show() You can also specify a subset of axes to aggregate, and control the legend placement: >>> fig, ax = plt.subplots(nrows=2, ncols=2, sharex=True) >>> ax[0, 0].plot(x, label='Line', color='C0') >>> ax[0, 1].plot(x**2, label='Parabola', color='C1') >>> ax[1, 0].plot(x**3, label='Cubic', color='C2') >>> ax[1, 1].plot(x**4, label='Quartic', color='C3') >>> librosa.display.legend_for_axes(axes=ax[0], loc='outside upper center') >>> librosa.display.legend_for_axes(axes=ax[1], loc='outside lower center') >>> plt.show() """ if axes is None: if fig is None: fig = plt.gcf() axes = fig.axes axes_array = np.atleast_1d(np.asarray(axes, dtype=object)) if len(axes_array.flat) == 0: raise ParameterError("No axes provided for legend aggregation") if fig is None: fig = axes_array.flat[0].figure for ax in axes_array.flat: if ax.figure is not fig: raise ParameterError("All axes must belong to the same figure") handles: list[Artist] = [] labels: list[str] = [] for ax in axes_array.flat: hlist, llist = ax.get_legend_handles_labels() handles.extend(hlist) labels.extend(llist) return fig.legend(handles, labels, **kwargs) def _get_ax_bright_highlight( ax: mplaxes.Axes, luminance_threshold: float = 0.5, ) -> bool: """Determine whether the axes should produce a bright or dark highlight. This is based on a few things: - If the axes has mappable data, we take the median color of that data. - If the axes has no mappable data, we take the facecolor of the axes. - If the axes is transparent, we take the facecolor of the figure. From the resulting color, we calculate the luminance by RGB->YIQ conversion. Luminance above threshold is considered light, and should therefore produce a dark highlight. Luminance below threshold is considered dark, and should produce a bright highlight. """ mappable = None for child in ax.get_children(): if isinstance(child, cm.ScalarMappable) and child.get_array() is not None: mappable = child break if mappable is not None: data = mappable.get_array() # Calculate median, ignoring NaNs median_val = np.nanmedian(np.asarray(data)) # Map through the normalization and colormap normed_val = mappable.norm(median_val) rgba = mappable.get_cmap()(normed_val) else: # If there's no mappable data, get the axes facecolor rgba = ax.get_facecolor() # And if the axes is transparent, pull from the figure if len(rgba) == 4 and rgba[3] == 0.0: rgba = ax.figure.get_facecolor() # Calculate relative luminance luminance = colorsys.rgb_to_yiq(*rgba[:3])[0] return luminance <= luminance_threshold def highlight( *, artist: Artist | None = None, ax: mplaxes.Axes | None = None, color: ColorType | None = None, bright_color: ColorType = "white", dark_color: ColorType = "black", luminance_threshold: float = 0.5, **kwargs: Any, ) -> list[mpe.AbstractPathEffect]: """Apply a contrasting highlight effect to a matplotlib artist. This is primarily useful for providing contrast between an artist (e.g., a line plot) and an underlying image (e.g., a spectrogram or scatter plot). For example, if the underlying image is predominantly dark (under the choice of colormap), then a bright highlight (default "white") should be used. If the underlying image is predominantly bright, then a dark highlight (default "black") should be used. This function is designed to automatically infer which kind of highlight should be applied based on the contents of the `ax` axes object, if any. If no color-mapped data can be identified on `ax`, then the axes facecolor or figure facecolor will be used as fallbacks. If an `artist` is provided, the highlight effect will be applied in-place, but this is optional. (See examples below.) The choices for bright and dark highlight colors, as well as the luminance threshold for determining which to use, can be customized via the `bright_color`, `dark_color`, and `luminance_threshold` parameters. Alternatively, the user can bypass the automatic color inference and directly specify a highlight color via the `color` parameter. Parameters ---------- artist : matplotlib.artist.Artist, optional The artist to which the highlight effect should be applied. If not provided, the function will still return the appropriate path effect object(s) based on the contents of `ax`, but will not apply them to any artist. ax : matplotlib.axes.Axes, optional The axes to inspect for color-mapped data to determine the appropriate highlight color. If not provided, the function will attempt to infer an appropriate axes object from `artist`, and if that fails, will default to the current axes (`plt.gca()`). color : color specifier, optional A color specification to use directly for the highlight, bypassing the automatic color inference. If not provided, the function will determine whether to use `bright_color` or `dark_color` based on the contents of `ax` and the `luminance_threshold`. bright_color : color specifier, default 'white' The color to use for the highlight if the underlying axes is determined to be dark. dark_color : color specifier, default 'black' The color to use for the highlight if the underlying axes is determined to be bright. luminance_threshold : float, default 0.5 The luminance threshold for determining whether the underlying axes is considered bright or dark. Luminance is calculated by converting the relevant color to YIQ color space and taking the Y (luminance) component. If the luminance is above this threshold, the axes is considered bright and `dark_color` will be used for the highlight. If the luminance is below this threshold, the axes is considered dark and `bright_color` will be used for the highlight. **kwargs : dict Additional keyword arguments to pass to `matplotlib.patheffects.withStroke` when creating the highlight effect. Common options include `linewidth` (default to 2) and `alpha` (default to 1.0). .. note:: `foreground`, if provided, will override the `color` parameter and the automatic color inference. To avoid confusion, it's recommended to specify highlight color via the `color` parameter and not to provide `foreground` in `kwargs`. Returns ------- effects : list of matplotlib.patheffects.AbstractPathEffect A list of path effect objects that implement the highlight. If `artist` was provided, these effects will have been applied to the artist in-place. If `artist` was not provided, these effects can be applied to any artist via `artist.set_path_effects(effects)`. Examples -------- Plotting an fâ‚€ contour with and without highlighting, in bright or dark colormaps >>> import matplotlib.pyplot as plt >>> y, sr = librosa.loadx('trumpet') >>> f0, _, _ = librosa.pyin(y, fmin=100, fmax=1000) >>> times = librosa.times_like(f0) >>> D = librosa.stft(y) >>> fig, ax = plt.subplots(nrows=2, ncols=2, sharex=True, sharey=True) >>> librosa.display.specshow(D, x_axis='time', y_axis='log_oct3', ax=ax[0, 0], ... vscale='dBFS') >>> ax[0, 0].plot(times, f0) >>> ax[0, 0].set_title('Dark image, no highlight') >>> librosa.display.specshow(D, x_axis='time', y_axis='log_oct3', ax=ax[0, 1], ... vscale='dBFS') >>> line = ax[0, 1].plot(times, f0)[0] # 'plot' returns a list of artists >>> librosa.display.highlight(artist=line) >>> ax[0, 1].set_title('Dark image, highlighted') >>> librosa.display.specshow(D, x_axis='time', y_axis='log_oct3', ax=ax[1, 0], ... vscale='dBFS', cmap='gray_r') >>> ax[1, 0].plot(times, f0) >>> ax[1, 0].set_title('Bright image, no highlight') >>> librosa.display.specshow(D, x_axis='time', y_axis='log_oct3', ax=ax[1, 1], ... vscale='dBFS', cmap='gray_r') >>> # We can also construct the highlight first and then supply it to the plot command >>> hl = librosa.display.highlight(ax=ax[1, 1]) >>> ax[1, 1].plot(times, f0, path_effects=hl) >>> ax[1, 1].set_title('Bright image, highlighted') >>> for a in ax.flat: ... a.label_outer() >>> plt.show() """ # 1. Resolve Axes if ax is None: if artist is not None and hasattr(artist, "axes") and artist.axes is not None: ax = cast("mplaxes.Axes", artist.axes) else: ax = plt.gca() # 2. Infer highlight color color = kwargs.pop("foreground", color) if color is None: if _get_ax_bright_highlight(ax, luminance_threshold): # Axes is dark, so we want a bright highlight stroke_color = bright_color else: # Axes is bright, so we want a dark highlight stroke_color = dark_color else: # Use the user-specified highlight color stroke_color = color kwargs.setdefault("linewidth", 2) kwargs.setdefault("alpha", 1.0) # 3. Create and apply the effect effects: list[mpe.AbstractPathEffect] = [mpe.withStroke(foreground=stroke_color, **kwargs)] if artist is not None: artist.set_path_effects(effects) return effects ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2112267 librosa-1.0.0/librosa/effects.py0000644000175100017510000007667315236640653016272 0ustar00runnerrunner#!/usr/bin/env python # -*- coding: utf-8 -*- """ librosa.effects provides wrapper functions for time-series manipulation, harmonic-percussive source separation, and standard audio effects. It also provides some basic functions for trimming, splitting, or splicing signals. """ from __future__ import annotations from typing import TYPE_CHECKING, overload import numpy as np from . import core, decompose, feature, util from .util.exceptions import ParameterError if TYPE_CHECKING: from typing import Any, Callable, Iterable, Literal from numpy.typing import ArrayLike from ._typing import ( _Array1D, _FloatLike_co, _IntLike_co, _PadModeSTFT, _WindowSpec, ) __all__ = [ "hpss", "harmonic", "percussive", "time_stretch", "pitch_shift", "remix", "trim", "split", "preemphasis", "deemphasis", ] def hpss( y: np.ndarray, *, kernel_size: _IntLike_co | tuple[_IntLike_co, _IntLike_co] | list[_IntLike_co] = 31, power: float = 2.0, mask: bool = False, margin: _FloatLike_co | tuple[_FloatLike_co, _FloatLike_co] | list[_FloatLike_co] = 1.0, n_fft: int = 2048, hop_length: int | None = None, win_length: int | None = None, window: _WindowSpec = "hann", center: bool = True, pad_mode: _PadModeSTFT = "constant", ) -> tuple[np.ndarray, np.ndarray]: """Decompose an audio time series into harmonic and percussive components. This function automates the STFT->HPSS->ISTFT pipeline, and ensures that the output waveforms have equal length to the input waveform ``y``. Parameters ---------- y : np.ndarray [shape=(..., n)] audio time series. Multi-channel is supported. kernel_size : int or tuple (kernel_harmonic, kernel_percussive) Kernel size(s) for the median filters. - If scalar, the same size is used for both harmonic and percussive. - If tuple, the first value specifies the width of the harmonic filter, and the second value specifies the width of the percussive filter. power : float > 0 [scalar] Exponent for the Wiener filter when constructing soft mask matrices. mask : bool Return the masking matrices instead of components. margin : float or tuple (margin_harmonic, margin_percussive) Margin size(s) for the masks. - If scalar, the same size is used for both harmonic and percussive. - If tuple, the first value specifies the margin of the harmonic mask, and the second value specifies the margin of the percussive mask. n_fft : int > 0 [scalar] Length of the windowed signal after padding with zeros. The number of rows in the STFT matrix is ``(1 + n_fft/2)``. hop_length : int or None Number of audio samples between adjacent STFT columns. If unspecified, defaults to ``win_length // 4``. win_length : int or None Each frame of audio is windowed by ``window`` of length ``win_length`` and then padded with zeros to match ``n_fft``. If unspecified, defaults to ``win_length = n_fft``. window : str, tuple, number, function, or np.ndarray [shape=(n_fft,)] Window specification. See `scipy.signal.get_window` for supported values. center : bool If ``True``, the signal is padded so that frame ``t`` is centered at ``y[t * hop_length]``. pad_mode : str Padding mode used when ``center=True``. See `numpy.pad` for available modes. Returns ------- y_harmonic : np.ndarray [shape=(..., n)] audio time series of the harmonic elements y_percussive : np.ndarray [shape=(..., n)] audio time series of the percussive elements See Also -------- harmonic : Extract only the harmonic component percussive : Extract only the percussive component librosa.decompose.hpss : HPSS on spectrograms Examples -------- >>> # Extract harmonic and percussive components >>> y, sr = librosa.loadx('choice') >>> y_harmonic, y_percussive = librosa.effects.hpss(y) >>> # Get a more isolated percussive component by widening its margin >>> y_harmonic, y_percussive = librosa.effects.hpss(y, margin=(1.0,5.0)) """ # Compute the STFT matrix stft = core.stft( y, n_fft=n_fft, hop_length=hop_length, win_length=win_length, center=center, pad_mode=pad_mode, ) # Decompose into harmonic and percussives stft_harm, stft_perc = decompose.hpss( stft, kernel_size=kernel_size, power=power, mask=mask, margin=margin ) # Invert the STFTs. Adjust length to match the input. y_harm = core.istft( stft_harm, dtype=y.dtype, n_fft=n_fft, hop_length=hop_length, win_length=win_length, center=center, length=y.shape[-1], ) y_perc = core.istft( stft_perc, dtype=y.dtype, n_fft=n_fft, hop_length=hop_length, win_length=win_length, center=center, length=y.shape[-1], ) return y_harm, y_perc def harmonic( y: np.ndarray, *, kernel_size: _IntLike_co | tuple[_IntLike_co, _IntLike_co] | list[_IntLike_co] = 31, power: float = 2.0, mask: bool = False, margin: _FloatLike_co | tuple[_FloatLike_co, _FloatLike_co] | list[_FloatLike_co] = 1.0, n_fft: int = 2048, hop_length: int | None = None, win_length: int | None = None, window: _WindowSpec = "hann", center: bool = True, pad_mode: _PadModeSTFT = "constant", ) -> np.ndarray: """Extract harmonic elements from an audio time-series. Parameters ---------- y : np.ndarray [shape=(..., n)] audio time series. Multi-channel is supported. kernel_size : int or tuple (kernel_harmonic, kernel_percussive) Kernel size(s) for the median filters. - If scalar, the same size is used for both harmonic and percussive. - If tuple, the first value specifies the width of the harmonic filter, and the second value specifies the width of the percussive filter. power : float > 0 [scalar] Exponent for the Wiener filter when constructing soft mask matrices. mask : bool Return the masking matrices instead of components. margin : float or tuple (margin_harmonic, margin_percussive) Margin size(s) for the masks. - If scalar, the same size is used for both harmonic and percussive. - If tuple, the first value specifies the margin of the harmonic mask, and the second value specifies the margin of the percussive mask. n_fft : int > 0 [scalar] Length of the windowed signal after padding with zeros. The number of rows in the STFT matrix is ``(1 + n_fft/2)``. hop_length : int or None Number of audio samples between adjacent STFT columns. If unspecified, defaults to ``win_length // 4``. win_length : int or None Each frame of audio is windowed by ``window`` of length ``win_length`` and then padded with zeros to match ``n_fft``. If unspecified, defaults to ``win_length = n_fft``. window : str, tuple, number, function, or np.ndarray [shape=(n_fft,)] Window specification. See `scipy.signal.get_window` for supported values. center : bool If ``True``, the signal is padded so that frame ``t`` is centered at ``y[t * hop_length]``. pad_mode : str Padding mode used when ``center=True``. See `numpy.pad` for available modes. Returns ------- y_harmonic : np.ndarray [shape=(..., n)] audio time series of just the harmonic portion See Also -------- hpss : Separate harmonic and percussive components percussive : Extract only the percussive component librosa.decompose.hpss : HPSS for spectrograms Examples -------- >>> # Extract harmonic component >>> y, sr = librosa.loadx('choice') >>> y_harmonic = librosa.effects.harmonic(y) >>> # Use a margin > 1.0 for greater harmonic separation >>> y_harmonic = librosa.effects.harmonic(y, margin=3.0) """ # Compute the STFT matrix stft = core.stft( y, n_fft=n_fft, hop_length=hop_length, win_length=win_length, center=center, pad_mode=pad_mode, ) # Remove percussives stft_harm = decompose.hpss( stft, kernel_size=kernel_size, power=power, mask=mask, margin=margin )[0] # Invert the STFTs y_harm = core.istft( stft_harm, dtype=y.dtype, n_fft=n_fft, hop_length=hop_length, win_length=win_length, center=center, length=y.shape[-1], ) return y_harm def percussive( y: np.ndarray, *, kernel_size: _IntLike_co | tuple[_IntLike_co, _IntLike_co] | list[_IntLike_co] = 31, power: float = 2.0, mask: bool = False, margin: _FloatLike_co | tuple[_FloatLike_co, _FloatLike_co] | list[_FloatLike_co] = 1.0, n_fft: int = 2048, hop_length: int | None = None, win_length: int | None = None, window: _WindowSpec = "hann", center: bool = True, pad_mode: _PadModeSTFT = "constant", ) -> np.ndarray: """Extract percussive elements from an audio time-series. Parameters ---------- y : np.ndarray [shape=(..., n)] audio time series. Multi-channel is supported. kernel_size : int or tuple (kernel_harmonic, kernel_percussive) Kernel size(s) for the median filters. - If scalar, the same size is used for both harmonic and percussive. - If tuple, the first value specifies the width of the harmonic filter, and the second value specifies the width of the percussive filter. power : float > 0 [scalar] Exponent for the Wiener filter when constructing soft mask matrices. mask : bool Return the masking matrices instead of components. margin : float or tuple (margin_harmonic, margin_percussive) Margin size(s) for the masks. - If scalar, the same size is used for both harmonic and percussive. - If tuple, the first value specifies the margin of the harmonic mask, and the second value specifies the margin of the percussive mask. n_fft : int > 0 [scalar] Length of the windowed signal after padding with zeros. The number of rows in the STFT matrix is ``(1 + n_fft/2)``. hop_length : int or None Number of audio samples between adjacent STFT columns. If unspecified, defaults to ``win_length // 4``. win_length : int or None Each frame of audio is windowed by ``window`` of length ``win_length`` and then padded with zeros to match ``n_fft``. If unspecified, defaults to ``win_length = n_fft``. window : str, tuple, number, function, or np.ndarray [shape=(n_fft,)] Window specification. See `scipy.signal.get_window` for supported values. center : bool If ``True``, the signal is padded so that frame ``t`` is centered at ``y[t * hop_length]``. pad_mode : str Padding mode used when ``center=True``. See `numpy.pad` for available modes. Returns ------- y_percussive : np.ndarray [shape=(..., n)] audio time series of just the percussive portion See Also -------- hpss : Separate harmonic and percussive components harmonic : Extract only the harmonic component librosa.decompose.hpss : HPSS for spectrograms Examples -------- >>> # Extract percussive component >>> y, sr = librosa.loadx('choice') >>> y_percussive = librosa.effects.percussive(y) >>> # Use a margin > 1.0 for greater percussive separation >>> y_percussive = librosa.effects.percussive(y, margin=3.0) """ # Compute the STFT matrix stft = core.stft( y, n_fft=n_fft, hop_length=hop_length, win_length=win_length, center=center, pad_mode=pad_mode, ) # Remove harmonics stft_perc = decompose.hpss( stft, kernel_size=kernel_size, power=power, mask=mask, margin=margin )[1] # Invert the STFT y_perc = core.istft( stft_perc, dtype=y.dtype, n_fft=n_fft, hop_length=hop_length, win_length=win_length, center=center, length=y.shape[-1], ) return y_perc def time_stretch(y: np.ndarray, *, rate: float, **kwargs: Any) -> np.ndarray: """Time-stretch an audio series by a fixed rate. Parameters ---------- y : np.ndarray [shape=(..., n)] audio time series. Multi-channel is supported. rate : float > 0 [scalar] Stretch factor. If ``rate > 1``, then the signal is sped up. If ``rate < 1``, then the signal is slowed down. **kwargs : additional keyword arguments. See `librosa.decompose.stft` for details. Returns ------- y_stretch : np.ndarray [shape=(..., round(n/rate))] audio time series stretched by the specified rate See Also -------- pitch_shift : Pitch shifting librosa.phase_vocoder : Spectrogram phase vocoder pyrubberband.pyrb.time_stretch : High-quality time stretching using RubberBand Examples -------- Compress to be twice as fast >>> y, sr = librosa.loadx('choice') >>> y_fast = librosa.effects.time_stretch(y, rate=2.0) Or half the original speed >>> y_slow = librosa.effects.time_stretch(y, rate=0.5) """ if rate <= 0: raise ParameterError("rate must be a positive number") # Construct the short-term Fourier transform (STFT) stft = core.stft(y, **kwargs) # Stretch by phase vocoding stft_stretch = core.phase_vocoder( stft, rate=rate, hop_length=kwargs.get("hop_length"), n_fft=kwargs.get("n_fft"), ) # Predict the length of y_stretch len_stretch = round(y.shape[-1] / rate) # Invert the STFT y_stretch = core.istft(stft_stretch, dtype=y.dtype, length=len_stretch, **kwargs) return y_stretch def pitch_shift( y: np.ndarray, *, sr: float, n_steps: float, bins_per_octave: int = 12, res_type: str = "soxr_hq", scale: bool = False, **kwargs: Any, ) -> np.ndarray: """Shift the pitch of a waveform by ``n_steps`` steps. A step is equal to a semitone if ``bins_per_octave`` is set to 12. Parameters ---------- y : np.ndarray [shape=(..., n)] audio time series. Multi-channel is supported. sr : number > 0 [scalar] audio sampling rate of ``y`` n_steps : float [scalar] how many (fractional) steps to shift ``y`` bins_per_octave : int > 0 [scalar] how many steps per octave res_type : str Resample type. By default, 'soxr_hq' is used. See `librosa.resample` for more information. scale : bool Scale the resampled signal so that ``y`` and ``y_hat`` have approximately equal total energy. **kwargs : additional keyword arguments. See `librosa.decompose.stft` for details. Returns ------- y_shift : np.ndarray [shape=(..., n)] The pitch-shifted audio time-series See Also -------- time_stretch : Time stretching librosa.phase_vocoder : Spectrogram phase vocoder pyrubberband.pyrb.pitch_shift : High-quality pitch shifting using RubberBand Examples -------- Shift up by a major third (four steps if ``bins_per_octave`` is 12) >>> y, sr = librosa.loadx('choice') >>> y_third = librosa.effects.pitch_shift(y, sr=sr, n_steps=4) Shift down by a tritone (six steps if ``bins_per_octave`` is 12) >>> y_tritone = librosa.effects.pitch_shift(y, sr=sr, n_steps=-6) Shift up by 3 quarter-tones >>> y_three_qt = librosa.effects.pitch_shift(y, sr=sr, n_steps=3, ... bins_per_octave=24) """ if not util.is_positive_int(bins_per_octave): raise ParameterError( f"bins_per_octave={bins_per_octave} must be a positive integer." ) rate = 2.0 ** (-float(n_steps) / bins_per_octave) # Stretch in time, then resample y_shift = core.resample( time_stretch(y, rate=rate, **kwargs), orig_sr=float(sr) / rate, target_sr=sr, res_type=res_type, scale=scale, ) # Crop to the same dimension as the input return util.fix_length(y_shift, size=y.shape[-1]) def remix( y: np.ndarray, intervals: Iterable[tuple[int, int]], *, align_zeros: bool = True ) -> np.ndarray: """Remix an audio signal by re-ordering time intervals. Parameters ---------- y : np.ndarray [shape=(..., t)] Audio time series. Multi-channel is supported. intervals : iterable of tuples (start, end) An iterable (list-like or generator) where the ``i``th item ``intervals[i]`` indicates the start and end (in samples) of a slice of ``y``. align_zeros : bool If ``True``, interval boundaries are mapped to the closest zero-crossing in ``y``. If ``y`` is stereo, zero-crossings are computed after converting to mono. Returns ------- y_remix : np.ndarray [shape=(..., d)] ``y`` remixed in the order specified by ``intervals`` Examples -------- Load in the example track and reverse the beats >>> y, sr = librosa.loadx('choice') Compute beats >>> _, beat_frames = librosa.beat.beat_track(y=y, sr=sr, ... hop_length=512) Convert from frames to sample indices >>> beat_samples = librosa.frames_to_samples(beat_frames) Generate intervals from consecutive events >>> intervals = librosa.util.frame(beat_samples, frame_length=2, ... hop_length=1).T Reverse the beat intervals >>> y_out = librosa.effects.remix(y, intervals[::-1]) """ y_out = [] if align_zeros: y_mono = core.to_mono(y) zeros = np.nonzero(core.zero_crossings(y_mono))[-1] # Force end-of-signal onto zeros zeros = np.append(zeros, [len(y_mono)]) for interval in intervals: if align_zeros: interval = zeros[util.match_events(interval, zeros)] y_out.append(y[..., interval[0] : interval[1]]) return np.concatenate(y_out, axis=-1) def _signal_to_frame_nonsilent( y: np.ndarray, frame_length: int = 2048, hop_length: int = 512, top_db: float = 60, ref: Callable | float = np.max, aggregate: Callable = np.max, ) -> _Array1D[np.bool]: """Frame-wise non-silent indicator for audio input. This is a helper function for `trim` and `split`. Parameters ---------- y : np.ndarray Audio signal, mono or stereo frame_length : int > 0 The number of samples per frame hop_length : int > 0 The number of samples between frames top_db : number The threshold (in decibels) below reference to consider as silence. You can also use a negative value for `top_db` to treat any value below `ref + |top_db|` as silent. This will only make sense if `ref` is not `np.max`. ref : callable or float The reference amplitude aggregate : callable [default: np.max] Function to aggregate dB measurements across channels (if y.ndim > 1) Note: for multiple leading axes, this is performed using ``np.apply_over_axes``. Returns ------- non_silent : np.ndarray, shape=(m,), dtype=bool Indicator of non-silent frames """ # Compute the MSE for the signal mse = feature.rms(y=y, frame_length=frame_length, hop_length=hop_length) # Convert to decibels and slice out the mse channel db: np.ndarray = core.amplitude_to_db(mse[..., 0, :], ref=ref, top_db=None) # Aggregate everything but the time dimension if db.ndim > 1: db = np.apply_over_axes(aggregate, db, range(db.ndim - 1)) # Squeeze out leading singleton dimensions here # We always want to keep the trailing dimension though db = np.squeeze(db, axis=tuple(range(db.ndim - 1))) return db > -top_db def trim( y: np.ndarray, *, top_db: float = 60, ref: float | Callable = np.max, frame_length: int = 2048, hop_length: int = 512, aggregate: Callable = np.max, ) -> tuple[np.ndarray, _Array1D[np.int_]]: """Trim leading and trailing silence from an audio signal. Silence is defined as segments of the audio signal that are `top_db` decibels (or more) quieter than a reference level, `ref`. By default, `ref` is set to the signal's maximum RMS value. It's important to note that if the entire signal maintains a uniform RMS value, there will be no segments considered quieter than the maximum, leading to no trimming. This implies that a completely silent signal will remain untrimmed with the default `ref` setting. In these situations, an explicit value for `ref` (in decibels) should be used instead. Parameters ---------- y : np.ndarray, shape=(..., n) Audio signal. Multi-channel is supported. top_db : number The threshold (in decibels) below reference to consider as silence. You can also use a negative value for `top_db` to treat any value below `ref + |top_db|` as silent. This will only make sense if `ref` is not `np.max`. ref : number or callable The reference amplitude. By default, it uses `np.max` and compares to the peak amplitude in the signal. frame_length : int > 0 The number of samples per analysis frame hop_length : int > 0 The number of samples between analysis frames aggregate : callable [default: np.max] Function to aggregate across channels (if y.ndim > 1) Returns ------- y_trimmed : np.ndarray, shape=(..., m) The trimmed signal index : np.ndarray, shape=(2,) the interval of ``y`` corresponding to the non-silent region: ``y_trimmed = y[index[0]:index[1]]`` (for mono) or ``y_trimmed = y[:, index[0]:index[1]]`` (for stereo). Examples -------- >>> # Load some audio >>> y, sr = librosa.loadx('choice') >>> # Trim the beginning and ending silence >>> yt, index = librosa.effects.trim(y) >>> # Print the durations >>> print(librosa.get_duration(y, sr=sr), librosa.get_duration(yt, sr=sr)) 25.025986394557822 25.007891156462584 """ non_silent = _signal_to_frame_nonsilent( y, frame_length=frame_length, hop_length=hop_length, ref=ref, top_db=top_db, aggregate=aggregate, ) nonzero = np.flatnonzero(non_silent) if nonzero.size > 0: # Compute the start and end positions # End position goes one frame past the last non-zero start = int(core.frames_to_samples(nonzero[0], hop_length=hop_length)) end = min( y.shape[-1], int(core.frames_to_samples(nonzero[-1] + 1, hop_length=hop_length)), ) else: # The entire signal is trimmed here: nothing is above the threshold start, end = 0, 0 # Slice the buffer and return the corresponding interval return y[..., start:end], np.asarray([start, end]) def split( y: np.ndarray, *, top_db: float = 60, ref: float | Callable = np.max, frame_length: int = 2048, hop_length: int = 512, aggregate: Callable = np.max, ) -> np.ndarray: """Split an audio signal into non-silent intervals. Parameters ---------- y : np.ndarray, shape=(..., n) An audio signal. Multi-channel is supported. top_db : number > 0 The threshold (in decibels) below reference to consider as silence ref : number or callable The reference amplitude. By default, it uses `np.max` and compares to the peak amplitude in the signal. frame_length : int > 0 The number of samples per analysis frame hop_length : int > 0 The number of samples between analysis frames aggregate : callable [default: np.max] Function to aggregate across channels (if y.ndim > 1) Returns ------- intervals : np.ndarray, shape=(m, 2) ``intervals[i] == (start_i, end_i)`` are the start and end time (in samples) of non-silent interval ``i``. """ non_silent = _signal_to_frame_nonsilent( y, frame_length=frame_length, hop_length=hop_length, ref=ref, top_db=top_db, aggregate=aggregate, ) # Interval slicing, adapted from # https://stackoverflow.com/questions/2619413/efficiently-finding-the-interval-with-non-zeros-in-scipy-numpy-in-python # Find points where the sign flips edges = np.flatnonzero(np.diff(non_silent.astype(int))) # Pad back the sample lost in the diff edges = [edges + 1] # If the first frame had high energy, count it if non_silent[0]: edges.insert(0, np.array([0])) # Likewise for the last frame if non_silent[-1]: edges.append(np.array([len(non_silent)])) # Convert from frames to samples edges = core.frames_to_samples(np.concatenate(edges), hop_length=hop_length) # Clip to the signal duration edges = np.minimum(edges, y.shape[-1], out=edges) # Stack the results back as an ndarray edge_out: np.ndarray = edges.reshape((-1, 2)) return edge_out @overload def preemphasis( y: np.ndarray, *, coef: float = ..., zi: ArrayLike | None = ..., return_zf: Literal[False] = ..., ) -> np.ndarray: ... @overload def preemphasis( y: np.ndarray, *, coef: float = ..., zi: ArrayLike | None = ..., return_zf: Literal[True], ) -> tuple[np.ndarray, np.ndarray]: ... def preemphasis( y: np.ndarray, *, coef: float = 0.97, zi: ArrayLike | None = None, return_zf: bool = False, ) -> np.ndarray | tuple[np.ndarray, np.ndarray]: """Pre-emphasize an audio signal with a first-order differencing filter: y[n] -> y[n] - coef * y[n-1] Parameters ---------- y : np.ndarray [shape=(..., n)] Audio signal. Multi-channel is supported. coef : positive number Pre-emphasis coefficient. Typical values of ``coef`` are between 0 and 1. At the limit ``coef=0``, the signal is unchanged. At ``coef=1``, the result is the first-order difference of the signal. The default (0.97) matches the pre-emphasis filter used in the HTK implementation of MFCCs [#]_. .. [#] https://htk.eng.cam.ac.uk/ zi : number Initial filter state. When making successive calls to non-overlapping frames, this can be set to the ``zf`` returned from the previous call. (See example below.) By default ``zi`` is initialized as ``2*y[0] - y[1]``. return_zf : bool If ``True``, return the final filter state. If ``False``, only return the pre-emphasized signal. Returns ------- y_out : np.ndarray pre-emphasized signal zf : number if ``return_zf=True``, the final filter state is also returned Examples -------- Apply a standard pre-emphasis filter >>> import matplotlib.pyplot as plt >>> y, sr = librosa.loadx('trumpet') >>> y_filt = librosa.effects.preemphasis(y) >>> # and plot the results for comparison >>> S_orig = librosa.amplitude_to_db(np.abs(librosa.stft(y)), ref=np.max, top_db=None) >>> S_preemph = librosa.amplitude_to_db(np.abs(librosa.stft(y_filt)), ref=np.max, top_db=None) >>> fig, ax = plt.subplots(nrows=2, sharex=True, sharey=True) >>> librosa.display.specshow(S_orig, y_axis='log', x_axis='time', ax=ax[0]) >>> ax[0].set(title='Original signal') >>> ax[0].label_outer() >>> img = librosa.display.specshow(S_preemph, y_axis='log', x_axis='time', ax=ax[1]) >>> ax[1].set(title='Pre-emphasized signal') >>> librosa.display.colorbar_db(img, ax=ax) Apply pre-emphasis in pieces for block streaming. Note that the second block initializes ``zi`` with the final state ``zf`` returned by the first call. >>> y_filt_1, zf = librosa.effects.preemphasis(y[:1000], return_zf=True) >>> y_filt_2, zf = librosa.effects.preemphasis(y[1000:], zi=zf, return_zf=True) >>> np.allclose(y_filt, np.concatenate([y_filt_1, y_filt_2])) True See Also -------- deemphasis """ b = np.asarray([1.0, -coef], dtype=y.dtype) a = np.asarray([1.0], dtype=y.dtype) if zi is None: # Initialize the filter to implement linear extrapolation zi = 2 * y[..., 0:1] - y[..., 1:2] zi = np.atleast_1d(zi) y_out: np.ndarray z_f: np.ndarray import scipy.signal y_out, z_f = scipy.signal.lfilter(b, a, y, zi=np.asarray(zi, dtype=y.dtype)) if return_zf: return y_out, z_f return y_out @overload def deemphasis( y: np.ndarray, *, coef: float = ..., zi: ArrayLike | None = ..., return_zf: Literal[False] = ..., ) -> np.ndarray: ... @overload def deemphasis( y: np.ndarray, *, coef: float = ..., zi: ArrayLike | None = ..., return_zf: Literal[True], ) -> tuple[np.ndarray, np.ndarray]: ... def deemphasis( y: np.ndarray, *, coef: float = 0.97, zi: ArrayLike | None = None, return_zf: bool = False, ) -> np.ndarray | tuple[np.ndarray, np.ndarray]: """De-emphasize an audio signal with the inverse operation of preemphasis(): If y = preemphasis(x, coef=coef, zi=zi), the deemphasis is: >>> x[i] = y[i] + coef * x[i-1] >>> x = deemphasis(y, coef=coef, zi=zi) Parameters ---------- y : np.ndarray [shape=(..., n)] Audio signal. Multi-channel is supported. coef : positive number Pre-emphasis coefficient. Typical values of ``coef`` are between 0 and 1. At the limit ``coef=0``, the signal is unchanged. At ``coef=1``, the result is the first-order difference of the signal. The default (0.97) matches the pre-emphasis filter used in the HTK implementation of MFCCs [#]_. .. [#] https://htk.eng.cam.ac.uk/ zi : number Initial filter state. If inverting a previous preemphasis(), the same value should be used. By default ``zi`` is initialized as ``((2 - coef) * y[0] - y[1]) / (3 - coef)``. This value corresponds to the transformation of the default initialization of ``zi`` in ``preemphasis()``, ``2*x[0] - x[1]``. return_zf : bool If ``True``, return the final filter state. If ``False``, only return the pre-emphasized signal. Returns ------- y_out : np.ndarray de-emphasized signal zf : number if ``return_zf=True``, the final filter state is also returned Examples -------- Apply a standard pre-emphasis filter and invert it with de-emphasis >>> y, sr = librosa.loadx('trumpet') >>> y_filt = librosa.effects.preemphasis(y) >>> y_deemph = librosa.effects.deemphasis(y_filt) >>> np.allclose(y, y_deemph) True See Also -------- preemphasis """ b = np.array([1.0, -coef], dtype=y.dtype) a = np.array([1.0], dtype=y.dtype) y_out: np.ndarray zf: np.ndarray import scipy.signal if zi is None: # initialize with all zeros zi = np.zeros([*list(y.shape[:-1]), 1], dtype=y.dtype) y_out, zf = scipy.signal.lfilter(a, b, y, zi=zi) # factor in the linear extrapolation y_out -= ( ((2 - coef) * y[..., 0:1] - y[..., 1:2]) / (3 - coef) * (coef ** np.arange(y.shape[-1])) ) else: zi = np.atleast_1d(zi) y_out, zf = scipy.signal.lfilter(a, b, y, zi=zi.astype(y.dtype)) if return_zf: return y_out, zf else: return y_out ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462640.9836147 librosa-1.0.0/librosa/feature/0000755000175100017510000000000015236640661015710 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2112267 librosa-1.0.0/librosa/feature/__init__.py0000644000175100017510000000051415236640653020022 0ustar00runnerrunner#!/usr/bin/env python # -*- coding: utf-8 -*- """librosa.feature contains routines for computing spectral, tonal, rhythmic features from audio signals. Utility functions for manipulating and inverting features are also provided. """ import lazy_loader as lazy __getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2125447 librosa-1.0.0/librosa/feature/__init__.pyi0000644000175100017510000000224015236640653020171 0ustar00runnerrunnerfrom . import inverse as inverse from .rhythm import fourier_tempogram as fourier_tempogram from .rhythm import hybrid_tempogram as hybrid_tempogram from .rhythm import metrogram as metrogram from .rhythm import tempo as tempo from .rhythm import tempogram as tempogram from .rhythm import tempogram_ratio as tempogram_ratio from .spectral import chroma_cens as chroma_cens from .spectral import chroma_cqt as chroma_cqt from .spectral import chroma_stft as chroma_stft from .spectral import chroma_vqt as chroma_vqt from .spectral import melspectrogram as melspectrogram from .spectral import mfcc as mfcc from .spectral import poly_features as poly_features from .spectral import rms as rms from .spectral import spectral_bandwidth as spectral_bandwidth from .spectral import spectral_centroid as spectral_centroid from .spectral import spectral_contrast as spectral_contrast from .spectral import spectral_flatness as spectral_flatness from .spectral import spectral_rolloff as spectral_rolloff from .spectral import tonnetz as tonnetz from .spectral import zero_crossing_rate as zero_crossing_rate from .utils import delta as delta from .utils import stack_memory as stack_memory ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2125447 librosa-1.0.0/librosa/feature/inverse.py0000644000175100017510000003160215236640653017740 0ustar00runnerrunner#!/usr/bin/env python # -*- coding: utf-8 -*- """Feature inversion""" from __future__ import annotations import warnings from typing import TYPE_CHECKING import numpy as np import scipy from .. import filters from ..core.spectrum import db_to_power, griffinlim from ..util import expand_to, nnls from ..util.exceptions import ParameterError from ..util.utils import tiny if TYPE_CHECKING: from typing import Any from numpy.typing import DTypeLike from .._typing import _DCTNorm, _DCTType, _PadModeSTFT, _WindowSpec __all__ = ["mel_to_stft", "mel_to_audio", "mfcc_to_mel", "mfcc_to_audio"] def mel_to_stft( M: np.ndarray, *, sr: float = 22050, n_fft: int = 2048, power: float = 2.0, **kwargs: Any, ) -> np.ndarray: """Approximate STFT magnitude from a Mel power spectrogram. Parameters ---------- M : np.ndarray [shape=(..., n_mels, n), non-negative] The spectrogram as produced by `feature.melspectrogram` sr : number > 0 [scalar] sampling rate of the underlying signal n_fft : int > 0 [scalar] length of the FFT frame of the resulting STFT; the output has ``1 + n_fft // 2`` frequency bins power : float > 0 [scalar] Exponent for the magnitude melspectrogram **kwargs : additional keyword arguments for Mel filter bank parameters fmin : float >= 0 [scalar] lowest frequency (in Hz) fmax : float >= 0 [scalar] highest frequency (in Hz). If `None`, use ``fmax = sr / 2.0`` htk : bool [scalar] use HTK formula instead of Slaney norm : {None, 'slaney', or number} [scalar] If 'slaney', divide the triangular mel weights by the width of the mel band (area normalization). If numeric, use `librosa.util.normalize` to normalize each filter by to unit l_p norm. See `librosa.util.normalize` for a full description of supported norm values (including `+-np.inf`). Otherwise, leave all the triangles aiming for a peak value of 1.0 dtype : np.dtype The data type of the output basis. By default, uses 32-bit (single-precision) floating point. Returns ------- S : np.ndarray [shape=(..., 1 + n_fft // 2, t), non-negative] An approximate linear magnitude spectrogram See Also -------- librosa.feature.melspectrogram librosa.stft librosa.filters.mel librosa.util.nnls Examples -------- >>> y, sr = librosa.loadx('trumpet') >>> S = librosa.util.abs2(librosa.stft(y)) >>> mel_spec = librosa.feature.melspectrogram(S=S, sr=sr) >>> S_inv = librosa.feature.inverse.mel_to_stft(mel_spec, sr=sr) Compare the results visually >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=3, sharex=True, sharey=True) >>> img = librosa.display.specshow(S, vscale='dBFS[power]', top_db=None, ... y_axis='log', x_axis='time', ax=ax[0]) >>> ax[0].set(title='Original STFT') >>> ax[0].label_outer() >>> librosa.display.specshow(S_inv, vscale='dBFS[power]', top_db=None, ... y_axis='log', x_axis='time', ax=ax[1]) >>> ax[1].set(title='Reconstructed STFT') >>> ax[1].label_outer() >>> librosa.display.specshow(S_inv - S, vscale=f'dB[power,{S.max()}]', top_db=None, ... vmax=0, y_axis='log', x_axis='time', ax=ax[2]) >>> ax[2].set(title='Residual error (dB)') >>> librosa.display.colorbar_db(img, ax=ax) """ # Construct a mel basis with dtype matching the input data mel_basis = filters.mel( sr=sr, n_fft=n_fft, n_mels=M.shape[-2], dtype=M.dtype, **kwargs ) # Find the non-negative least squares solution, and apply # the inverse exponent. # We'll do the exponentiation in-place. inverse = nnls(mel_basis, M) np.power(inverse, 1.0 / power, out=inverse) return inverse def mel_to_audio( M: np.ndarray, *, sr: float = 22050, n_fft: int = 2048, hop_length: int | None = None, win_length: int | None = None, window: _WindowSpec = "hann", center: bool = True, pad_mode: _PadModeSTFT = "constant", power: float = 2.0, n_iter: int = 32, length: int | None = None, dtype: DTypeLike = np.float32, **kwargs: Any, ) -> np.ndarray: """Invert a mel power spectrogram to audio using Griffin-Lim. This is primarily a convenience wrapper for: >>> S = librosa.feature.inverse.mel_to_stft(M) >>> y = librosa.griffinlim(S) Parameters ---------- M : np.ndarray [shape=(..., n_mels, n), non-negative] The spectrogram as produced by `feature.melspectrogram` sr : number > 0 [scalar] sampling rate of the underlying signal n_fft : int > 0 [scalar] length of the FFT frame for the intermediate STFT hop_length : None or int > 0 The hop length of the STFT. If not provided, it will default to ``n_fft // 4`` win_length : None or int > 0 The window length of the STFT. By default, it will equal ``n_fft`` window : str, tuple, number, function, or np.ndarray [shape=(n_fft,)] A window specification as supported by `stft` or `istft` center : bool If `True`, the STFT is assumed to use centered frames. If `False`, the STFT is assumed to use left-aligned frames. pad_mode : str If ``center=True``, the padding mode to use at the edges of the signal. By default, STFT uses zero padding. power : float > 0 [scalar] Exponent for the magnitude melspectrogram n_iter : int > 0 The number of iterations for Griffin-Lim length : None or int > 0 If provided, the output ``y`` is zero-padded or clipped to exactly ``length`` samples. dtype : np.dtype Real numeric type for the time-domain signal. Default is 32-bit float. **kwargs : additional keyword arguments for Mel filter bank parameters fmin : float >= 0 [scalar] lowest frequency (in Hz) fmax : float >= 0 [scalar] highest frequency (in Hz). If `None`, use ``fmax = sr / 2.0`` htk : bool [scalar] use HTK formula instead of Slaney norm : {None, 'slaney', or number} [scalar] If 'slaney', divide the triangular mel weights by the width of the mel band (area normalization). If numeric, use `librosa.util.normalize` to normalize each filter by to unit l_p norm. See `librosa.util.normalize` for a full description of supported norm values (including `+-np.inf`). Otherwise, leave all the triangles aiming for a peak value of 1.0 Returns ------- y : np.ndarray [shape(..., n,)] time-domain signal reconstructed from ``M`` See Also -------- librosa.griffinlim librosa.feature.melspectrogram librosa.filters.mel librosa.feature.inverse.mel_to_stft """ stft = mel_to_stft(M, sr=sr, n_fft=n_fft, power=power, **kwargs) return griffinlim( stft, n_iter=n_iter, hop_length=hop_length, win_length=win_length, n_fft=n_fft, window=window, center=center, dtype=dtype, length=length, pad_mode=pad_mode, ) def mfcc_to_mel( mfcc: np.ndarray, *, n_mels: int = 128, dct_type: _DCTType = 2, norm: _DCTNorm | None = "ortho", ref: float = 1.0, lifter: float = 0, ) -> np.ndarray: """Invert Mel-frequency cepstral coefficients to approximate a Mel power spectrogram. This inversion proceeds in two steps: 1. The inverse DCT is applied to the MFCCs 2. `librosa.db_to_power` is applied to map the dB-scaled result to a power spectrogram Parameters ---------- mfcc : np.ndarray [shape=(..., n_mfcc, n)] The Mel-frequency cepstral coefficients n_mels : int > 0 The number of Mel frequencies dct_type : {1, 2, 3} Discrete cosine transform (DCT) type By default, DCT type-2 is used. norm : None or 'ortho' If ``dct_type`` is `2 or 3`, setting ``norm='ortho'`` uses an orthonormal DCT basis. Normalization is not supported for `dct_type=1`. ref : float Reference power for (inverse) decibel calculation lifter : number >= 0 If ``lifter>0``, apply inverse liftering (inverse cepstral filtering):: M[n, :] <- M[n, :] / (1 + sin(pi * (n + 1) / lifter) * lifter / 2) Returns ------- M : np.ndarray [shape=(..., n_mels, n)] An approximate Mel power spectrum recovered from ``mfcc`` Warns ----- UserWarning due to critical values in lifter array that invokes underflow. See Also -------- librosa.feature.mfcc librosa.feature.melspectrogram scipy.fft.dct """ if lifter > 0: n_mfcc = mfcc.shape[-2] idx = np.arange(1, 1 + n_mfcc, dtype=mfcc.dtype) idx = expand_to(idx, ndim=mfcc.ndim, axes=-2) lifter_sine = 1 + lifter * 0.5 * np.sin(np.pi * idx / lifter) # raise a UserWarning if lifter array includes critical values if np.any(np.abs(lifter_sine) < np.finfo(lifter_sine.dtype).eps): warnings.warn( message="lifter array includes critical values that may invoke underflow.", category=UserWarning, stacklevel=2, ) # lifter mfcc values mfcc = mfcc / (lifter_sine + tiny(mfcc)) elif lifter != 0: raise ParameterError("MFCC to mel lifter must be a non-negative number.") logmel = scipy.fft.idct(mfcc, axis=-2, type=dct_type, norm=norm, n=n_mels) melspec: np.ndarray = db_to_power(logmel, ref=ref) return melspec def mfcc_to_audio( mfcc: np.ndarray, *, n_mels: int = 128, dct_type: _DCTType = 2, norm: _DCTNorm | None = "ortho", ref: float = 1.0, lifter: float = 0, **kwargs: Any, ) -> np.ndarray: """Convert Mel-frequency cepstral coefficients to a time-domain audio signal This function is primarily a convenience wrapper for the following steps: 1. Convert mfcc to Mel power spectrum (`mfcc_to_mel`) 2. Convert Mel power spectrum to time-domain audio (`mel_to_audio`) Parameters ---------- mfcc : np.ndarray [shape=(..., n_mfcc, n)] The Mel-frequency cepstral coefficients n_mels : int > 0 The number of Mel frequencies dct_type : {1, 2, 3} Discrete cosine transform (DCT) type By default, DCT type-2 is used. norm : None or 'ortho' If ``dct_type`` is `2 or 3`, setting ``norm='ortho'`` uses an orthonormal DCT basis. Normalization is not supported for ``dct_type=1``. ref : float Reference power for (inverse) decibel calculation lifter : number >= 0 If ``lifter>0``, apply inverse liftering (inverse cepstral filtering):: M[n, :] <- M[n, :] / (1 + sin(pi * (n + 1) / lifter)) * lifter / 2 **kwargs additional keyword arguments to pass through to `mel_to_audio` M : np.ndarray [shape=(..., n_mels, n), non-negative] The spectrogram as produced by `feature.melspectrogram` sr : number > 0 [scalar] sampling rate of the underlying signal n_fft : int > 0 [scalar] length of the FFT frame for the intermediate STFT hop_length : None or int > 0 The hop length of the STFT. If not provided, it will default to ``n_fft // 4`` win_length : None or int > 0 The window length of the STFT. By default, it will equal ``n_fft`` window : str, tuple, number, function, or np.ndarray [shape=(n_fft,)] A window specification as supported by `stft` or `istft` center : bool If `True`, the STFT is assumed to use centered frames. If `False`, the STFT is assumed to use left-aligned frames. pad_mode : str If ``center=True``, the padding mode to use at the edges of the signal. By default, STFT uses zero padding. power : float > 0 [scalar] Exponent for the magnitude melspectrogram n_iter : int > 0 The number of iterations for Griffin-Lim length : None or int > 0 If provided, the output ``y`` is zero-padded or clipped to exactly ``length`` samples. dtype : np.dtype Real numeric type for the time-domain signal. Default is 32-bit float. **kwargs additional keyword arguments for Mel filter bank parameters fmin : float >= 0 [scalar] lowest frequency (in Hz) fmax : float >= 0 [scalar] highest frequency (in Hz). If `None`, use ``fmax = sr / 2.0`` htk : bool [scalar] use HTK formula instead of Slaney Returns ------- y : np.ndarray [shape=(..., n)] A time-domain signal reconstructed from `mfcc` See Also -------- mfcc_to_mel mel_to_audio librosa.feature.mfcc librosa.griffinlim scipy.fft.dct """ mel_spec = mfcc_to_mel( mfcc, n_mels=n_mels, dct_type=dct_type, norm=norm, ref=ref, lifter=lifter ) return mel_to_audio(mel_spec, **kwargs) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2125447 librosa-1.0.0/librosa/feature/rhythm.py0000644000175100017510000010273615236640653017607 0ustar00runnerrunner#!/usr/bin/env python # -*- coding: utf-8 -*- """Rhythmic feature extraction""" from __future__ import annotations from typing import TYPE_CHECKING import numpy as np from .. import util from .._cache import cache from ..core.audio import autocorrelate from ..core.convert import fourier_tempo_frequencies, tempo_frequencies, time_to_frames from ..core.harmonic import f0_harmonics, interp_harmonics from ..core.spectrum import stft from ..filters import get_window from ..util.exceptions import ParameterError if TYPE_CHECKING: from typing import Any, Callable import scipy from .._typing import _InterpKind, _WindowSpec __all__ = [ "tempogram", "fourier_tempogram", "tempo", "tempogram_ratio", "metrogram", "hybrid_tempogram", ] # -- Rhythmic features -- # def tempogram( *, y: np.ndarray | None = None, sr: float = 22050, onset_envelope: np.ndarray | None = None, hop_length: int = 512, win_length: int = 384, center: bool = True, window: _WindowSpec = "hann", norm: float | None = np.inf, ) -> np.ndarray: """Compute the tempogram: local autocorrelation of the onset strength envelope. [#]_ .. [#] Grosche, Peter, Meinard Müller, and Frank Kurth. "Cyclic tempogram - A mid-level tempo representation for music signals." ICASSP, 2010. Parameters ---------- y : np.ndarray [shape=(..., n)] or None Audio time series. Multi-channel is supported. sr : number > 0 [scalar] sampling rate of ``y`` onset_envelope : np.ndarray [shape=(..., n) or (..., m, n)] or None Optional pre-computed onset strength envelope as provided by `librosa.onset.onset_strength`. If multi-dimensional, tempograms are computed independently for each band (first dimension). hop_length : int > 0 number of audio samples between successive onset measurements win_length : int > 0 length of the onset autocorrelation window (in frames/onset measurements) The default settings (384) corresponds to ``384 * hop_length / sr ~= 8.9s``. center : bool If `True`, onset autocorrelation windows are centered. If `False`, windows are left-aligned. window : str, function, number, tuple, or np.ndarray [shape=(win_length,)] A window specification as in `stft`. norm : {np.inf, -np.inf, 0, float > 0, None} Normalization mode. Set to `None` to disable normalization. Returns ------- tempogram : np.ndarray [shape=(..., win_length, n)] Localized autocorrelation of the onset strength envelope. If given multi-band input (``onset_envelope.shape==(m,n)``) then ``tempogram[i]`` is the tempogram of ``onset_envelope[i]``. Raises ------ ParameterError if neither ``y`` nor ``onset_envelope`` are provided if ``win_length < 1`` See Also -------- fourier_tempogram librosa.onset.onset_strength librosa.util.normalize librosa.stft Examples -------- >>> # Compute local onset autocorrelation >>> y, sr = librosa.loadx('nutcracker', duration=30) >>> hop_length = 512 >>> oenv = librosa.onset.onset_strength(y=y, sr=sr, hop_length=hop_length) >>> tempogram = librosa.feature.tempogram(onset_envelope=oenv, sr=sr, ... hop_length=hop_length) >>> # Compute global onset autocorrelation >>> ac_global = librosa.autocorrelate(oenv, max_size=tempogram.shape[0]) >>> ac_global = librosa.util.normalize(ac_global) >>> # Estimate the global tempo for display purposes >>> tempo = librosa.feature.tempo(onset_envelope=oenv, sr=sr, ... hop_length=hop_length)[0] >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=4, figsize=(10, 10)) >>> times = librosa.times_like(oenv, sr=sr, hop_length=hop_length) >>> ax[0].plot(times, oenv, label='Onset strength') >>> ax[0].label_outer() >>> ax[0].legend(frameon=True) >>> librosa.display.specshow(tempogram, sr=sr, hop_length=hop_length, ... x_axis='time', y_axis='tempo', cmap='magma', ... ax=ax[1]) >>> hl = librosa.display.highlight(ax=ax[1], linewidth=4, color='w', alpha=.75) >>> ax[1].axhline(tempo, path_effects=hl, linewidth=2, color='k', linestyle='--', ... label='Estimated tempo={:g}'.format(tempo)) >>> ax[1].legend(loc='upper right') >>> ax[1].set(title='Tempogram') >>> x = np.linspace(0, tempogram.shape[0] * float(hop_length) / sr, ... num=tempogram.shape[0]) >>> ax[2].plot(x, np.mean(tempogram, axis=1), label='Mean local autocorrelation') >>> ax[2].plot(x, ac_global, '--', alpha=0.75, label='Global autocorrelation') >>> ax[2].set(xlabel='Lag (seconds)') >>> ax[2].legend(frameon=True) >>> freqs = librosa.tempo_frequencies(tempogram.shape[0], hop_length=hop_length, sr=sr) >>> ax[3].semilogx(freqs[1:], np.mean(tempogram[1:], axis=1), ... label='Mean local autocorrelation', base=2) >>> ax[3].semilogx(freqs[1:], ac_global[1:], '--', alpha=0.75, ... label='Global autocorrelation', base=2) >>> ax[3].axvline(tempo, color='black', linestyle='--', alpha=.8, ... label='Estimated tempo={:g}'.format(tempo)) >>> ax[3].legend(frameon=True) >>> ax[3].set(xlabel='BPM') >>> ax[3].grid(True) """ from ..onset import onset_strength if win_length < 1: raise ParameterError("win_length must be a positive integer") ac_window = get_window(window, win_length, fftbins=True) if onset_envelope is None: if y is None: raise ParameterError("Either y or onset_envelope must be provided") onset_envelope = onset_strength(y=y, sr=sr, hop_length=hop_length) # Center the autocorrelation windows n = onset_envelope.shape[-1] if center: padding = [(0, 0) for _ in onset_envelope.shape] padding[-1] = (int(win_length // 2),) * 2 onset_envelope = np.pad( onset_envelope, padding, mode="linear_ramp", end_values=[0, 0] ) # Carve onset envelope into frames odf_frame = util.frame(onset_envelope, frame_length=win_length, hop_length=1) # Truncate to the length of the original signal if center: odf_frame = odf_frame[..., :n] # explicit broadcast of ac_window ac_window = util.expand_to(ac_window, ndim=odf_frame.ndim, axes=-2) # Window, autocorrelate, and normalize return util.normalize( autocorrelate(odf_frame * ac_window, axis=-2), norm=norm, axis=-2 ) def fourier_tempogram( *, y: np.ndarray | None = None, sr: float = 22050, onset_envelope: np.ndarray | None = None, hop_length: int = 512, win_length: int = 384, center: bool = True, window: _WindowSpec = "hann", ) -> np.ndarray: """Compute the Fourier tempogram: the short-time Fourier transform of the onset strength envelope. [#]_ .. [#] Grosche, Peter, Meinard Müller, and Frank Kurth. "Cyclic tempogram - A mid-level tempo representation for music signals." ICASSP, 2010. Parameters ---------- y : np.ndarray [shape=(..., n)] or None Audio time series. Multi-channel is supported. sr : number > 0 [scalar] sampling rate of ``y`` onset_envelope : np.ndarray [shape=(..., n)] or None Optional pre-computed onset strength envelope as provided by ``librosa.onset.onset_strength``. Multi-channel is supported. hop_length : int > 0 number of audio samples between successive onset measurements win_length : int > 0 length of the onset window (in frames/onset measurements) The default settings (384) corresponds to ``384 * hop_length / sr ~= 8.9s``. center : bool If `True`, onset windows are centered. If `False`, windows are left-aligned. window : str, function, number, tuple, or np.ndarray [shape=(win_length,)] A window specification as in `stft`. Returns ------- tempogram : np.ndarray [shape=(..., win_length // 2 + 1, n)] Complex short-time Fourier transform of the onset envelope. Raises ------ ParameterError if neither ``y`` nor ``onset_envelope`` are provided if ``win_length < 1`` See Also -------- tempogram librosa.onset.onset_strength librosa.util.normalize librosa.stft Examples -------- >>> # Compute local onset autocorrelation >>> y, sr = librosa.loadx('nutcracker') >>> hop_length = 512 >>> oenv = librosa.onset.onset_strength(y=y, sr=sr, hop_length=hop_length) >>> tempogram = librosa.feature.fourier_tempogram(onset_envelope=oenv, sr=sr, ... hop_length=hop_length) >>> # Compute the auto-correlation tempogram, unnormalized to make comparison easier >>> ac_tempogram = librosa.feature.tempogram(onset_envelope=oenv, sr=sr, ... hop_length=hop_length, norm=None) >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=3, sharex=True) >>> ax[0].plot(librosa.times_like(oenv), oenv, label='Onset strength') >>> ax[0].legend(frameon=True) >>> ax[0].label_outer() >>> librosa.display.specshow(np.abs(tempogram), sr=sr, hop_length=hop_length, >>> x_axis='time', y_axis='fourier_tempo', cmap='magma', ... ax=ax[1]) >>> ax[1].set(title='Fourier tempogram') >>> ax[1].label_outer() >>> librosa.display.specshow(ac_tempogram, sr=sr, hop_length=hop_length, >>> x_axis='time', y_axis='tempo', cmap='magma', ... ax=ax[2]) >>> ax[2].set(title='Autocorrelation tempogram') """ from ..onset import onset_strength if win_length < 1: raise ParameterError("win_length must be a positive integer") if onset_envelope is None: if y is None: raise ParameterError("Either y or onset_envelope must be provided") onset_envelope = onset_strength(y=y, sr=sr, hop_length=hop_length) # Generate the short-time Fourier transform return stft( onset_envelope, n_fft=win_length, hop_length=1, center=center, window=window ) @cache(level=30) def tempo( *, y: np.ndarray | None = None, sr: float = 22050, onset_envelope: np.ndarray | None = None, tg: np.ndarray | None = None, hop_length: int = 512, start_bpm: float = 120, std_bpm: float = 1.0, ac_size: float = 8.0, max_tempo: float | None = 320.0, aggregate: Callable[..., Any] | None = np.mean, prior: scipy.stats.rv_continuous | None = None, ) -> np.ndarray: """Estimate the tempo (beats per minute) Parameters ---------- y : np.ndarray [shape=(..., n)] or None audio time series. Multi-channel is supported. sr : number > 0 [scalar] sampling rate of the time series onset_envelope : np.ndarray [shape=(..., n)] pre-computed onset strength envelope tg : np.ndarray pre-computed tempogram. If provided, then `y` and `onset_envelope` are ignored, and `win_length` is inferred from the shape of the tempogram. hop_length : int > 0 [scalar] hop length of the time series start_bpm : float [scalar] initial guess of the BPM std_bpm : float > 0 [scalar] standard deviation of tempo distribution ac_size : float > 0 [scalar] length (in seconds) of the auto-correlation window max_tempo : float > 0 [scalar, optional] If provided, only estimate tempo below this threshold aggregate : callable [optional] Aggregation function for estimating global tempo. If `None`, then tempo is estimated independently for each frame. prior : scipy.stats.rv_continuous [optional] A prior distribution over tempo (in beats per minute). By default, a pseudo-log-normal prior is used. If given, ``start_bpm`` and ``std_bpm`` will be ignored. Returns ------- tempo : np.ndarray estimated tempo (beats per minute). If input is multi-channel, one tempo estimate per channel is provided. See Also -------- librosa.onset.onset_strength librosa.feature.tempogram Notes ----- This function caches at level 30. Examples -------- >>> # Estimate a static tempo >>> y, sr = librosa.loadx('nutcracker', duration=30) >>> onset_env = librosa.onset.onset_strength(y=y, sr=sr) >>> tempo = librosa.feature.tempo(onset_envelope=onset_env, sr=sr) >>> tempo array([143.555]) >>> # Or a static tempo with a uniform prior instead >>> import scipy.stats >>> prior = scipy.stats.uniform(30, 300) # uniform over 30-300 BPM >>> utempo = librosa.feature.tempo(onset_envelope=onset_env, sr=sr, prior=prior) >>> utempo array([161.499]) >>> # Or a dynamic tempo >>> dtempo = librosa.feature.tempo(onset_envelope=onset_env, sr=sr, ... aggregate=None) >>> dtempo array([ 89.103, 89.103, 89.103, ..., 123.047, 123.047, 123.047]) >>> # Dynamic tempo with a proper log-normal prior >>> prior_lognorm = scipy.stats.lognorm(loc=np.log(120), scale=120, s=1) >>> dtempo_lognorm = librosa.feature.tempo(onset_envelope=onset_env, sr=sr, ... aggregate=None, ... prior=prior_lognorm) >>> dtempo_lognorm array([ 89.103, 89.103, 89.103, ..., 123.047, 123.047, 123.047]) Plot the estimated tempo against the onset autocorrelation >>> import matplotlib.pyplot as plt >>> # Convert to scalar >>> tempo = tempo.item() >>> utempo = utempo.item() >>> # Compute 2-second windowed autocorrelation >>> hop_length = 512 >>> ac = librosa.autocorrelate(onset_env, max_size=2 * sr // hop_length) >>> freqs = librosa.tempo_frequencies(len(ac), sr=sr, ... hop_length=hop_length) >>> # Plot on a BPM axis. We skip the first (0-lag) bin. >>> fig, ax = plt.subplots() >>> ax.semilogx(freqs[1:], librosa.util.normalize(ac)[1:], ... label='Onset autocorrelation', base=2) >>> ax.axvline(tempo, 0, 1, alpha=0.75, linestyle='--', color='r', ... label='Tempo (default prior): {:.2f} BPM'.format(tempo)) >>> ax.axvline(utempo, 0, 1, alpha=0.75, linestyle=':', color='g', ... label='Tempo (uniform prior): {:.2f} BPM'.format(utempo)) >>> ax.set(xlabel='Tempo (BPM)', title='Static tempo estimation') >>> ax.grid(True) >>> ax.legend() >>> plt.show() Plot dynamic tempo estimates over a tempogram >>> fig, ax = plt.subplots() >>> tg = librosa.feature.tempogram(onset_envelope=onset_env, sr=sr, ... hop_length=hop_length) >>> librosa.display.specshow(tg, x_axis='time', y_axis='tempo', cmap='magma', ax=ax) >>> hl = librosa.display.highlight(ax=ax, alpha=.85, linewidth=3, color='k') >>> ax.plot(librosa.times_like(dtempo), dtempo, linewidth=2, ... path_effects=hl, label='Tempo estimate (default prior)') >>> ax.plot(librosa.times_like(dtempo_lognorm), dtempo_lognorm, color='C0', ... linestyle='--', linewidth=2, ... path_effects=hl, label='Tempo estimate (lognorm prior)') >>> ax.set(title='Dynamic tempo estimation') >>> ax.legend() >>> plt.show() """ if start_bpm <= 0: raise ParameterError("start_bpm must be strictly positive") if tg is None: win_length = time_to_frames(ac_size, sr=sr, hop_length=hop_length).item() tg = tempogram( y=y, sr=sr, onset_envelope=onset_envelope, hop_length=hop_length, win_length=win_length, ) else: # Override window length by what's actually given win_length = tg.shape[-2] # Eventually, we want this to work for time-varying tempo if aggregate is not None: tg = aggregate(tg, axis=-1, keepdims=True) assert tg is not None # Get the BPM values for each bin, skipping the 0-lag bin bpms = tempo_frequencies(win_length, hop_length=hop_length, sr=sr) # Weight the autocorrelation by a log-normal distribution if prior is None: logprior = -0.5 * ((np.log2(bpms) - np.log2(start_bpm)) / std_bpm) ** 2 else: logprior = prior.logpdf(bpms) # Kill everything above the max tempo if max_tempo is not None: max_idx = int(np.argmax(bpms < max_tempo)) logprior[:max_idx] = -np.inf # explicit axis expansion logprior = util.expand_to(logprior, ndim=tg.ndim, axes=-2) # Get the maximum, weighted by the prior # Using log1p here for numerical stability best_period = np.argmax(np.log1p(1e6 * tg) + logprior, axis=-2) tempo_est: np.ndarray = np.take(bpms, best_period) return tempo_est @cache(level=40) def tempogram_ratio( *, y: np.ndarray | None = None, sr: float = 22050, onset_envelope: np.ndarray | None = None, tg: np.ndarray | None = None, bpm: np.ndarray | None = None, hop_length: int = 512, win_length: int = 384, start_bpm: float = 120, std_bpm: float = 1.0, max_tempo: float | None = 320.0, freqs: np.ndarray | None = None, factors: np.ndarray | None = None, aggregate: Callable[..., Any] | None = None, prior: scipy.stats.rv_continuous | None = None, center: bool = True, window: _WindowSpec = "hann", kind: _InterpKind = "linear", fill_value: float = 0, norm: float | None = np.inf, ) -> np.ndarray: """Tempogram ratio features, also known as spectral rhythm patterns. [1]_ This function summarizes the energy at metrically important multiples of the tempo. For example, if the tempo corresponds to the quarter-note period, the tempogram ratio will measure the energy at the eighth note, sixteenth note, half note, whole note, etc. periods, as well as dotted and triplet ratios. By default, the multiplicative factors used here are as specified by [2]_. If the estimated tempo corresponds to a quarter note, these factors will measure relative energy at the following metrical subdivisions: +-------+--------+------------------+ | Index | Factor | Description | +=======+========+==================+ | 0 | 4 | Sixteenth note | +-------+--------+------------------+ | 1 | 8/3 | Dotted sixteenth | +-------+--------+------------------+ | 2 | 3 | Eighth triplet | +-------+--------+------------------+ | 3 | 2 | Eighth note | +-------+--------+------------------+ | 4 | 4/3 | Dotted eighth | +-------+--------+------------------+ | 5 | 3/2 | Quarter triplet | +-------+--------+------------------+ | 6 | 1 | Quarter note | +-------+--------+------------------+ | 7 | 2/3 | Dotted quarter | +-------+--------+------------------+ | 8 | 3/4 | Half triplet | +-------+--------+------------------+ | 9 | 1/2 | Half note | +-------+--------+------------------+ | 10 | 1/3 | Dotted half note | +-------+--------+------------------+ | 11 | 3/8 | Whole triplet | +-------+--------+------------------+ | 12 | 1/4 | Whole note | +-------+--------+------------------+ .. [1] Peeters, Geoffroy. "Rhythm Classification Using Spectral Rhythm Patterns." In ISMIR, pp. 644-647. 2005. .. [2] Prockup, Matthew, Andreas F. Ehmann, Fabien Gouyon, Erik M. Schmidt, and Youngmoo E. Kim. "Modeling musical rhythm at scale with the music genome project." In 2015 IEEE workshop on applications of signal processing to audio and acoustics (WASPAA), pp. 1-5. IEEE, 2015. Parameters ---------- y : np.ndarray [shape=(..., n)] or None audio time series sr : number > 0 [scalar] sampling rate of the time series onset_envelope : np.ndarray [shape=(..., n)] pre-computed onset strength envelope tg : np.ndarray pre-computed tempogram. If provided, then `y` and `onset_envelope` are ignored, and `win_length` is inferred from the shape of the tempogram. bpm : np.ndarray pre-computed tempo estimate. This must be a per-frame estimate, and have dimension compatible with `tg`. hop_length : int > 0 [scalar] hop length of the time series win_length : int > 0 [scalar] window length of the autocorrelation window for tempogram calculation start_bpm : float [scalar] initial guess of the BPM if `bpm` is not provided std_bpm : float > 0 [scalar] standard deviation of tempo distribution max_tempo : float > 0 [scalar, optional] If provided, only estimate tempo below this threshold freqs : np.ndarray Frequencies (in BPM) of the tempogram axis. factors : np.ndarray Multiples of the fundamental tempo (bpm) to estimate. If not provided, the factors are as specified above. aggregate : callable [optional] Aggregation function for estimating global tempogram ratio. If `None`, then ratios are estimated independently for each frame. prior : scipy.stats.rv_continuous [optional] A prior distribution over tempo (in beats per minute). By default, a pseudo-log-normal prior is used. If given, ``start_bpm`` and ``std_bpm`` will be ignored. center : bool If `True`, onset windows are centered. If `False`, windows are left-aligned. window : str, function, number, tuple, or np.ndarray [shape=(win_length,)] A window specification as in `stft`. kind : str Interpolation mode for measuring tempogram ratios fill_value : float The value to fill when extrapolating beyond the observed frequency range. norm : {np.inf, -np.inf, 0, float > 0, None} Normalization mode. Set to `None` to disable normalization. Returns ------- tgr : np.ndarray The tempogram ratio for the specified factors. If `aggregate` is provided, the trailing time axis will be removed. If `aggregate` is not provided (default), ratios will be estimated for each frame. See Also -------- tempogram tempo librosa.f0_harmonics librosa.tempo_frequencies Examples -------- Compute tempogram ratio features using the default factors for a waltz (3/4 time) >>> import matplotlib.pyplot as plt >>> y, sr = librosa.loadx('sweetwaltz') >>> tempogram = librosa.feature.tempogram(y=y, sr=sr) >>> tgr = librosa.feature.tempogram_ratio(tg=tempogram, sr=sr) >>> fig, ax = plt.subplots(nrows=2, sharex=True) >>> librosa.display.specshow(tempogram, x_axis='time', y_axis='tempo', ... ax=ax[0]) >>> librosa.display.specshow(tgr, x_axis='time', ax=ax[1]) >>> ax[0].label_outer() >>> ax[0].set(title="Tempogram") >>> ax[1].set(title="Tempogram ratio") """ # Get a tempogram and time-varying tempo estimate if tg is None: tg = tempogram( y=y, sr=sr, onset_envelope=onset_envelope, hop_length=hop_length, win_length=win_length, center=center, window=window, norm=norm, ) if freqs is None: freqs = tempo_frequencies(sr=sr, n_bins=len(tg), hop_length=hop_length) # Estimate tempo per-frame, no aggregation yet if bpm is None: bpm = tempo( sr=sr, tg=tg, hop_length=hop_length, start_bpm=start_bpm, std_bpm=std_bpm, max_tempo=max_tempo, aggregate=None, prior=prior, ) if factors is None: # metric multiples from Prockup'15 factors = np.array( [4, 8 / 3, 3, 2, 4 / 3, 3 / 2, 1, 2 / 3, 3 / 4, 1 / 2, 1 / 3, 3 / 8, 1 / 4] ) tgr = f0_harmonics( tg, freqs=freqs, f0=bpm, harmonics=factors, kind=kind, fill_value=fill_value ) if aggregate is not None: return aggregate(tgr, axis=-1) # type: ignore return tgr def hybrid_tempogram( *, y: np.ndarray | None = None, sr: float = 22050, onset_envelope: np.ndarray | None = None, hop_length: int = 512, win_length: int = 384, center: bool = True, window: _WindowSpec = "hann", **kwargs: Any, ) -> np.ndarray: """Compute a hybrid tempogram. This function computes a hybrid representation by combining the Fourier tempogram and autocorrelation tempogram. The tempograms are aligned onto a common frequency grid and merged using the geometric mean [1]_. .. [1] Peeters, Geoffroy. "Rhythm Classification Using Periodicities and the Beat-Histogram." Proceedings of the 6th International Conference on Music Information Retrieval (ISMIR). 2005. Parameters ---------- y : np.ndarray [shape=(..., n)] or None Audio time series. Multi-channel is supported. sr : float > 0 Sampling rate onset_envelope : np.ndarray [shape=(..., n)] or None Optional pre-computed onset strength envelope hop_length : int > 0 Number of samples between frames win_length : int > 0 Window length for analysis center : bool Whether to center the frames window : str, tuple, number, function, or np.ndarray [shape=(win_length,)] A window specification as supported by `scipy.signal.get_window` and `librosa.filters.get_window`. **kwargs : additional keyword arguments Additional keyword arguments passed to `scipy.interpolate.interp1d` Returns ------- hybrid : np.ndarray The hybrid tempogram combining both representations See Also -------- tempogram fourier_tempogram Examples -------- Compute local onset autocorrelation >>> y, sr = librosa.loadx('nutcracker') >>> hop_length = 512 >>> oenv = librosa.onset.onset_strength(y=y, sr=sr, hop_length=hop_length) Compute the autocorrelation, Fourier, and hybrid tempograms >>> tempogram = librosa.feature.tempogram(onset_envelope=oenv, sr=sr, ... hop_length=hop_length) >>> fourier_tempogram = librosa.feature.fourier_tempogram(onset_envelope=oenv, sr=sr, ... hop_length=hop_length) >>> hybrid_tempogram = librosa.feature.hybrid_tempogram(onset_envelope=oenv, sr=sr, ... hop_length=hop_length) Plot the results >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=3, sharex=True) >>> librosa.display.specshow(tempogram, x_axis='time', y_axis='tempo', ... hop_length=hop_length, ax=ax[0]) >>> ax[0].set(title='Autocorrelation Tempogram') >>> ax[0].label_outer() >>> librosa.display.specshow(np.abs(fourier_tempogram), x_axis='time', ... y_axis='fourier_tempo', hop_length=hop_length, ax=ax[1]) >>> ax[1].set(title='Fourier Tempogram') >>> ax[1].label_outer() >>> img = librosa.display.specshow(hybrid_tempogram, x_axis='time', ... y_axis='fourier_tempo', hop_length=hop_length, ... ax=ax[2]) >>> ax[2].set(title='Hybrid Tempogram') >>> fig.colorbar(img, ax=ax) """ interp_kwargs_dict: dict[str, Any] = kwargs if kwargs else {} interp_kwargs_dict.setdefault("bounds_error", False) interp_kwargs_dict.setdefault("fill_value", 0.0) interp_kwargs_dict.setdefault("copy", False) interp_kwargs_dict.setdefault("axis", -2) # Calculate onset envelope once to avoid redundant STFT computations if onset_envelope is None: if y is None: raise ParameterError("Either y or onset_envelope must be provided") from ..onset import onset_strength onset_envelope = onset_strength(y=y, sr=sr, hop_length=hop_length) # 1. Compute Fourier tempogram tg_f = fourier_tempogram( y=y, sr=sr, onset_envelope=onset_envelope, hop_length=hop_length, win_length=win_length, center=center, window=window, ) # Get Fourier tempogram frequencies freqs = fourier_tempo_frequencies( sr=sr, hop_length=hop_length, win_length=win_length ) # 2. Compute Autocorrelation tempogram tg_a = tempogram( y=y, sr=sr, onset_envelope=onset_envelope, hop_length=hop_length, win_length=win_length, center=center, window=window, ) # Get autocorrelation tempogram frequencies (lags) lags = tempo_frequencies(tg_a.shape[-2], sr=sr, hop_length=hop_length) # 3. Restrict to finite frequencies (drop 0-lag / infinite BPM) tg_a_finite = tg_a[..., 1:, :] lags_finite = lags[1:] # 4. Hybrid Interpolation import scipy.interpolate f_interp = scipy.interpolate.interp1d( lags_finite, tg_a_finite, **interp_kwargs_dict ) tg_a_resampled = f_interp(freqs) # 5. Shape Matching - align time frames n_frames_min = min(tg_f.shape[-1], tg_a_resampled.shape[-1]) # 6. Merging (Geometric Mean) product = np.abs(tg_f[..., :n_frames_min]) * np.abs(tg_a_resampled[..., :n_frames_min]) hybrid = np.sqrt(np.maximum(0, product)) return np.asarray(hybrid) @cache(level=40) def metrogram( *, tg: np.ndarray, freqs: np.ndarray, factors: np.ndarray | None = None, aggregate: Callable[..., Any] | None = np.sum, kind: _InterpKind = "linear", fill_value: float = 0, ) -> np.ndarray: """Metrogram Transform. [1]_ This function summarizes the presence of rhythmic ratios in a tempogram. For example, a tempogram with two simultaneous energy peaks at 90BPM and 30BPM would have a strong presence of the 1/3 ratio. This makes it possible to perform meter estimation by finding the ratio between the beat's and downbeat's frequency. By default, the factors used here are as specified by [2]_. +-------+--------+----------------+ | Index | Factor | Time Signature | +=======+========+================+ | 0 | 1/3 | 3/4 | +-------+--------+----------------+ | 1 | 1/4 | 4/4 | +-------+--------+----------------+ | 2 | 1/5 | 5/4 | +-------+--------+----------------+ | 3 | 1/7 | 7/4 | +-------+--------+----------------+ .. [1] Cozens, James, and Simon Godsill. "Dynamic Time Signature Recognition, Tempo Inference, and Beat Tracking Through the Metrogram Transform." In IEEE Open Journal of Signal Processing, pp. 1--9, 2023. .. [2] Abimbola, Jeremiah, Daniel Kostrzewa, and PaweÅ‚ Kasprowski. "METER2800: A novel dataset for music time signature detection." In Data in Brief, vol. 51, 109736, 2023. See Also -------- tempogram tempogram_ratio Parameters ---------- tg : np.ndarray Pre-computed tempogram. freqs : np.ndarray Frequencies (in BPM) of the tempogram axis. factors : np.ndarray Metric ratios to estimate. If not provided, the default factors are 1/3, 1/4, 1/5, and 1/7. aggregate : callable [optional] Aggregation function to collapse the tempo axis for each ratio at each point in time. Defaults to ``np.sum``. kind : str Interpolation method used on the tempo axis. fill_value : float The value to fill when extrapolating beyond the observed tempo range. Returns ------- metrogram : np.ndarray The metrogram transform for the specified factors. If ``aggregate`` is set to ``None``, the ratios for all individual tempo bins are returned. Examples -------- >>> import numpy as np >>> import matplotlib.pyplot as plt >>> import librosa >>> y, sr = librosa.loadx("sweetwaltz") >>> # extend the window, to capture the slower downbeat pulses >>> win_length = 384 * 4 >>> fourier_tempogram = librosa.feature.fourier_tempogram(y=y, win_length=win_length) >>> fourier_freqs = librosa.fourier_tempo_frequencies(win_length=win_length) >>> ac_tempogram = librosa.feature.tempogram(y=y, win_length=win_length) >>> ac_freqs = librosa.tempo_frequencies(ac_tempogram.shape[-2]) >>> # combine Fourier and AC tempo grid (alternatively, you may use either one) >>> # we remove np.inf from ac_freqs to avoid nan results >>> funt_freqs = np.union1d(fourier_freqs, ac_freqs[1:]) >>> fundamental_tempogram = librosa.util.interp_broadcast( ... x1=ac_tempogram, ... x1_pos=ac_freqs, ... x2=fourier_tempogram[..., :-1], # both tempograms must be of equal length along time ... x2_pos=fourier_freqs, ... interp_pos=funt_freqs, ... ) >>> metrogram = librosa.feature.metrogram(tg=fundamental_tempogram, freqs=funt_freqs) >>> fig, ax = plt.subplots() >>> librosa.display.specshow(np.abs(metrogram), x_axis="time", ax=ax) >>> ax.set(title="Metrogram") """ if factors is None: factors = np.array([1 / 3, 1 / 4, 1 / 5, 1 / 7]) tg_interp = interp_harmonics( tg, freqs=freqs, harmonics=factors, kind=kind, fill_value=fill_value, axis=-2 ) product: np.ndarray = tg_interp * np.expand_dims(tg, axis=-3) if aggregate is not None: product_agg: np.ndarray = aggregate(product, axis=-2) return product_agg return product ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2125447 librosa-1.0.0/librosa/feature/spectral.py0000644000175100017510000022470615236640653020113 0ustar00runnerrunner#!/usr/bin/env python # -*- coding: utf-8 -*- """Spectral feature extraction""" from __future__ import annotations import itertools from typing import TYPE_CHECKING import numpy as np from .. import filters, util from ..core.audio import zero_crossings from ..core.constantq import cqt, hybrid_cqt, vqt from ..core.convert import fft_frequencies from ..core.pitch import estimate_tuning from ..core.spectrum import _spectrogram, power_to_db from ..util.exceptions import ParameterError if TYPE_CHECKING: from typing import Any, Collection, Literal from numpy.typing import DTypeLike from .._typing import _DCTNorm, _DCTType, _FloatLike_co, _PadMode, _PadModeSTFT, _WindowSpec __all__ = [ "spectral_centroid", "spectral_bandwidth", "spectral_contrast", "spectral_rolloff", "spectral_flatness", "poly_features", "rms", "zero_crossing_rate", "chroma_stft", "chroma_cqt", "chroma_cens", "chroma_vqt", "melspectrogram", "mfcc", "tonnetz", ] # -- Spectral features -- # def spectral_centroid( *, y: np.ndarray | None = None, sr: float = 22050, S: np.ndarray | None = None, n_fft: int = 2048, hop_length: int = 512, freq: np.ndarray | None = None, win_length: int | None = None, window: _WindowSpec = "hann", center: bool = True, pad_mode: _PadModeSTFT = "constant", ) -> np.ndarray: """Compute the spectral centroid. Each frame of a magnitude spectrogram is normalized and treated as a distribution over frequency bins, from which the mean (centroid) is extracted per frame. More precisely, the centroid at frame ``t`` is defined as [#]_:: centroid[t] = sum_k S[k, t] * freq[k] / (sum_j S[j, t]) where ``S`` is a magnitude spectrogram, and ``freq`` is the array of frequencies (e.g., FFT frequencies in Hz) of the rows of ``S``. .. [#] Klapuri, A., & Davy, M. (Eds.). (2007). Signal processing methods for music transcription, chapter 5. Springer Science & Business Media. Parameters ---------- y : np.ndarray [shape=(..., n,)] or None audio time series. Multi-channel is supported. sr : number > 0 [scalar] audio sampling rate of ``y`` S : np.ndarray [shape=(..., d, t)] or None (optional) spectrogram magnitude n_fft : int > 0 [scalar] length of the FFT frame hop_length : int > 0 [scalar] hop length for STFT. See `librosa.stft` for details. freq : None or np.ndarray [shape=(d,) or shape=(d, t)] Center frequencies for spectrogram bins. If `None`, then FFT bin center frequencies are used. Otherwise, it can be a single array of ``d`` center frequencies, or a matrix of center frequencies as constructed by `librosa.reassigned_spectrogram` win_length : int <= n_fft [scalar] Each frame of audio is windowed by `window()`. The window will be of length ``win_length`` and then padded with zeros to match ``n_fft``. If unspecified, defaults to ``win_length = n_fft``. window : str, tuple, number, function, or np.ndarray [shape=(n_fft,)] - a window specification (str, tuple, or number); see `scipy.signal.get_window` - a window function, such as `scipy.signal.windows.hann` - a vector or array of length ``n_fft`` .. see also:: `librosa.filters.get_window` center : bool - If `True`, the signal ``y`` is padded so that frame `t` is centered at ``y[t * hop_length]``. - If `False`, then frame ``t`` begins at ``y[t * hop_length]`` pad_mode : str If ``center=True``, the padding mode to use at the edges of the signal. By default, STFT uses zero padding. Returns ------- centroid : np.ndarray [shape=(..., 1, t)] centroid frequencies See Also -------- librosa.stft : Short-time Fourier Transform librosa.reassigned_spectrogram : Time-frequency reassigned spectrogram Examples -------- From time-series input: >>> y, sr = librosa.loadx('trumpet') >>> cent = librosa.feature.spectral_centroid(y=y, sr=sr) >>> cent array([[1768.888, 1921.774, ..., 5663.477, 5813.683]]) From spectrogram input: >>> S, phase = librosa.magphase(librosa.stft(y=y)) >>> librosa.feature.spectral_centroid(S=S) array([[1768.888, 1921.774, ..., 5663.477, 5813.683]]) Using variable bin center frequencies: >>> freqs, times, D = librosa.reassigned_spectrogram(y, fill_nan=True) >>> librosa.feature.spectral_centroid(S=np.abs(D), freq=freqs) array([[1768.838, 1921.801, ..., 5663.513, 5813.747]]) Plot the result >>> import matplotlib.pyplot as plt >>> times = librosa.times_like(cent) >>> fig, ax = plt.subplots() >>> librosa.display.specshow(S, vscale='dBFS', ... y_axis='log', x_axis='time', ax=ax) >>> hl = librosa.display.highlight(ax=ax, color='k', linewidth=3, alpha=0.5) >>> ax.plot(times, cent.T, label='Spectral centroid', color='w', path_effects=hl) >>> ax.legend(loc='upper right') >>> ax.set(title='log Power spectrogram') """ # input is time domain:y or spectrogram:s # S, n_fft = _spectrogram( y=y, S=S, n_fft=n_fft, hop_length=hop_length, win_length=win_length, window=window, center=center, pad_mode=pad_mode, ) if not np.isrealobj(S): raise ParameterError( "Spectral centroid is only defined " "with real-valued input" ) elif np.any(S < 0): raise ParameterError( "Spectral centroid is only defined " "with non-negative energies" ) # Compute the center frequencies of each bin if freq is None: freq = fft_frequencies(sr=sr, n_fft=n_fft) if freq.ndim == 1: # reshape for broadcasting freq = util.expand_to(freq, ndim=S.ndim, axes=-2) # Column-normalize S centroid: np.ndarray = np.sum( freq * util.normalize(S, norm=1, axis=-2), axis=-2, keepdims=True ) return centroid def spectral_bandwidth( *, y: np.ndarray | None = None, sr: float = 22050, S: np.ndarray | None = None, n_fft: int = 2048, hop_length: int = 512, win_length: int | None = None, window: _WindowSpec = "hann", center: bool = True, pad_mode: _PadModeSTFT = "constant", freq: np.ndarray | None = None, centroid: np.ndarray | None = None, norm: bool = True, p: float = 2, ) -> np.ndarray: """Compute p'th-order spectral bandwidth. The spectral bandwidth [#]_ at frame ``t`` is computed by:: (sum_k S[k, t] * (freq[k, t] - centroid[t])**p)**(1/p) .. [#] Klapuri, A., & Davy, M. (Eds.). (2007). Signal processing methods for music transcription, chapter 5. Springer Science & Business Media. Parameters ---------- y : np.ndarray [shape=(..., n)] or None audio time series. Multi-channel is supported. sr : number > 0 [scalar] audio sampling rate of ``y`` S : np.ndarray [shape=(..., d, t)] or None (optional) spectrogram magnitude n_fft : int > 0 [scalar] length of the FFT frame hop_length : int > 0 [scalar] hop length for STFT. See `librosa.stft` for details. win_length : int <= n_fft [scalar] Each frame of audio is windowed by `window()`. The window will be of length ``win_length`` and then padded with zeros to match ``n_fft``. If unspecified, defaults to ``win_length = n_fft``. window : str, tuple, number, function, or np.ndarray [shape=(n_fft,)] - a window specification (str, tuple, or number); see `scipy.signal.get_window` - a window function, such as `scipy.signal.windows.hann` - a vector or array of length ``n_fft`` .. see also:: `librosa.filters.get_window` center : bool - If `True`, the signal ``y`` is padded so that frame ``t`` is centered at ``y[t * hop_length]``. - If ``False``, then frame ``t`` begins at ``y[t * hop_length]`` pad_mode : str If ``center=True``, the padding mode to use at the edges of the signal. By default, STFT uses zero padding. freq : None or np.ndarray [shape=(d,) or shape=(..., d, t)] Center frequencies for spectrogram bins. If `None`, then FFT bin center frequencies are used. Otherwise, it can be a single array of ``d`` center frequencies, or a matrix of center frequencies as constructed by `librosa.reassigned_spectrogram` centroid : None or np.ndarray [shape=(..., 1, t)] pre-computed centroid frequencies norm : bool Normalize per-frame spectral energy (sum to one) p : float > 0 Power to raise deviation from spectral centroid. Returns ------- bandwidth : np.ndarray [shape=(..., 1, t)] frequency bandwidth for each frame Examples -------- From time-series input >>> y, sr = librosa.loadx('trumpet') >>> spec_bw = librosa.feature.spectral_bandwidth(y=y, sr=sr) >>> spec_bw array([[1273.836, 1228.873, ..., 2952.357, 3013.68 ]]) From spectrogram input >>> S, phase = librosa.magphase(librosa.stft(y=y)) >>> librosa.feature.spectral_bandwidth(S=S) array([[1273.836, 1228.873, ..., 2952.357, 3013.68 ]]) Using variable bin center frequencies >>> freqs, times, D = librosa.reassigned_spectrogram(y, fill_nan=True) >>> librosa.feature.spectral_bandwidth(S=np.abs(D), freq=freqs) array([[1274.637, 1228.786, ..., 2952.4 , 3013.735]]) Plot the result >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=2, sharex=True) >>> times = librosa.times_like(spec_bw) >>> centroid = librosa.feature.spectral_centroid(S=S) >>> ax[0].semilogy(times, spec_bw[0], label='Spectral bandwidth') >>> ax[0].set(ylabel='Hz', xticks=[], xlim=[times.min(), times.max()]) >>> ax[0].legend() >>> ax[0].label_outer() >>> librosa.display.specshow(S, vscale='dBFS', ... y_axis='log', x_axis='time', ax=ax[1]) >>> ax[1].set(title='log Power spectrogram') >>> ax[1].fill_between(times, np.maximum(0, centroid[0] - spec_bw[0]), ... np.minimum(centroid[0] + spec_bw[0], sr/2), ... alpha=0.5, label='Centroid +- bandwidth') >>> hl = librosa.display.highlight(ax=ax[1], color='k', linewidth=3, alpha=0.5) >>> ax[1].plot(times, centroid[0], label='Spectral centroid', color='w', path_effects=hl) >>> ax[1].legend(loc='lower right') """ S, n_fft = _spectrogram( y=y, S=S, n_fft=n_fft, hop_length=hop_length, win_length=win_length, window=window, center=center, pad_mode=pad_mode, ) if not np.isrealobj(S): raise ParameterError( "Spectral bandwidth is only defined " "with real-valued input" ) elif np.any(S < 0): raise ParameterError( "Spectral bandwidth is only defined " "with non-negative energies" ) # If we don't have a centroid provided, compute it using the existing # spectrogram S if centroid is None: centroid = spectral_centroid( y=y, sr=sr, S=S, n_fft=n_fft, hop_length=hop_length, freq=freq ) # Compute the center frequencies of each bin if freq is None: freq = fft_frequencies(sr=sr, n_fft=n_fft) if freq.ndim == 1: deviation = np.abs( np.subtract.outer(centroid[..., 0, :], freq).swapaxes(-2, -1) ) else: deviation = np.abs(freq - centroid) # Column-normalize S if norm: S = util.normalize(S, norm=1, axis=-2) bw: np.ndarray = np.sum(S * deviation**p, axis=-2, keepdims=True) ** (1.0 / p) return bw def spectral_contrast( *, y: np.ndarray | None = None, sr: float = 22050, S: np.ndarray | None = None, n_fft: int = 2048, hop_length: int = 512, win_length: int | None = None, window: _WindowSpec = "hann", center: bool = True, pad_mode: _PadModeSTFT = "constant", freq: np.ndarray | None = None, fmin: float = 200.0, n_bands: int = 6, quantile: float = 0.02, linear: bool = False, ) -> np.ndarray: """Compute spectral contrast Each frame of a spectrogram ``S`` is divided into sub-bands. For each sub-band, the energy contrast is estimated by comparing the mean energy in the top quantile (peak energy) to that of the bottom quantile (valley energy). High contrast values generally correspond to clear, narrow-band signals, while low contrast values correspond to broad-band noise. [#]_ .. [#] Jiang, Dan-Ning, Lie Lu, Hong-Jiang Zhang, Jian-Hua Tao, and Lian-Hong Cai. "Music type classification by spectral contrast feature." In Multimedia and Expo, 2002. ICME'02. Proceedings. 2002 IEEE International Conference on, vol. 1, pp. 113-116. IEEE, 2002. Parameters ---------- y : np.ndarray [shape=(..., n)] or None audio time series. Multi-channel is supported. sr : number > 0 [scalar] audio sampling rate of ``y`` S : np.ndarray [shape=(..., d, t)] or None (optional) spectrogram magnitude n_fft : int > 0 [scalar] length of the FFT frame hop_length : int > 0 [scalar] hop length for STFT. See `librosa.stft` for details. win_length : int <= n_fft [scalar] Each frame of audio is windowed by `window()`. The window will be of length `win_length` and then padded with zeros to match ``n_fft``. If unspecified, defaults to ``win_length = n_fft``. window : str, tuple, number, function, or np.ndarray [shape=(n_fft,)] - a window specification (str, tuple, or number); see `scipy.signal.get_window` - a window function, such as `scipy.signal.windows.hann` - a vector or array of length ``n_fft`` .. see also:: `librosa.filters.get_window` center : bool - If `True`, the signal ``y`` is padded so that frame ``t`` is centered at ``y[t * hop_length]``. - If `False`, then frame ``t`` begins at ``y[t * hop_length]`` pad_mode : str If ``center=True``, the padding mode to use at the edges of the signal. By default, STFT uses zero padding. freq : None or np.ndarray [shape=(d,)] Center frequencies for spectrogram bins. If `None`, then FFT bin center frequencies are used. Otherwise, it can be a single array of ``d`` center frequencies. fmin : float > 0 Frequency cutoff for the first bin ``[0, fmin]`` Subsequent bins will cover ``[fmin, 2*fmin]`, `[2*fmin, 4*fmin]``, etc. n_bands : int > 1 number of frequency bands quantile : float in (0, 1) quantile for determining peaks and valleys linear : bool If `True`, return the linear difference of magnitudes: ``peaks - valleys``. If `False`, return the logarithmic difference: ``log(peaks) - log(valleys)``. Returns ------- contrast : np.ndarray [shape=(..., n_bands + 1, t)] each row of spectral contrast values corresponds to a given octave-based frequency Examples -------- >>> y, sr = librosa.loadx('trumpet') >>> S = np.abs(librosa.stft(y)) >>> contrast = librosa.feature.spectral_contrast(S=S, sr=sr) >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=2, sharex=True) >>> img1 = librosa.display.specshow(S, vscale='dBFS', ... y_axis='log', x_axis='time', ax=ax[0]) >>> librosa.display.colorbar_db(img1) >>> ax[0].set(title='Power spectrogram') >>> ax[0].label_outer() >>> img2 = librosa.display.specshow(contrast, x_axis='time', ax=ax[1]) >>> fig.colorbar(img2, ax=[ax[1]]) >>> ax[1].set(ylabel='Frequency bands', title='Spectral contrast') """ S, n_fft = _spectrogram( y=y, S=S, n_fft=n_fft, hop_length=hop_length, win_length=win_length, window=window, center=center, pad_mode=pad_mode, ) # Compute the center frequencies of each bin if freq is None: freq = fft_frequencies(sr=sr, n_fft=n_fft) freq = np.atleast_1d(freq) if freq.ndim != 1 or len(freq) != S.shape[-2]: raise ParameterError(f"freq.shape mismatch: expected ({S.shape[-2]:d},)") if n_bands < 1 or not isinstance(n_bands, (int, np.integer)): raise ParameterError("n_bands must be a positive integer") if not 0.0 < quantile < 1.0: raise ParameterError("quantile must lie in the range (0, 1)") if fmin <= 0: raise ParameterError("fmin must be a positive number") octa = np.zeros(n_bands + 2) octa[1:] = fmin * (2.0 ** np.arange(0, n_bands + 1)) if np.any(octa[:-1] >= 0.5 * sr): raise ParameterError( "Frequency band exceeds Nyquist. " "Reduce either fmin or n_bands." ) # shape of valleys and peaks based on spectrogram shape = list(S.shape) shape[-2] = n_bands + 1 valley = np.zeros(shape) peak = np.zeros_like(valley) for k, (f_low, f_high) in enumerate(itertools.pairwise(octa)): current_band = np.logical_and(freq >= f_low, freq <= f_high) idx = np.flatnonzero(current_band) if k > 0: current_band[idx[0] - 1] = True if k == n_bands: current_band[idx[-1] + 1 :] = True sub_band = S[..., current_band, :] if k < n_bands: sub_band = sub_band[..., :-1, :] # Always take at least one bin from each side idx = np.rint(quantile * np.sum(current_band)) idx = int(np.maximum(idx, 1)) sortedr = np.sort(sub_band, axis=-2) valley[..., k, :] = np.mean(sortedr[..., :idx, :], axis=-2) peak[..., k, :] = np.mean(sortedr[..., -idx:, :], axis=-2) contrast: np.ndarray if linear: contrast = peak - valley else: contrast = power_to_db(peak) - power_to_db(valley) return contrast def spectral_rolloff( *, y: np.ndarray | None = None, sr: float = 22050, S: np.ndarray | None = None, n_fft: int = 2048, hop_length: int = 512, win_length: int | None = None, window: _WindowSpec = "hann", center: bool = True, pad_mode: _PadModeSTFT = "constant", freq: np.ndarray | None = None, roll_percent: float = 0.85, ) -> np.ndarray: """Compute roll-off frequency. The roll-off frequency is defined for each frame as the center frequency for a spectrogram bin such that at least roll_percent (0.85 by default) of the energy of the spectrum in this frame is contained in this bin and the bins below. This can be used to, e.g., approximate the maximum (or minimum) frequency by setting roll_percent to a value close to 1 (or 0). Parameters ---------- y : np.ndarray [shape=(..., n)] or None audio time series. Multi-channel is supported. sr : number > 0 [scalar] audio sampling rate of ``y`` S : np.ndarray [shape=(d, t)] or None (optional) spectrogram magnitude n_fft : int > 0 [scalar] length of the FFT frame hop_length : int > 0 [scalar] hop length for STFT. See `librosa.stft` for details. win_length : int <= n_fft [scalar] Each frame of audio is windowed by `window()`. The window will be of length `win_length` and then padded with zeros to match ``n_fft``. If unspecified, defaults to ``win_length = n_fft``. window : str, tuple, number, function, or np.ndarray [shape=(n_fft,)] - a window specification (str, tuple, or number); see `scipy.signal.get_window` - a window function, such as `scipy.signal.windows.hann` - a vector or array of length ``n_fft`` .. see also:: `librosa.filters.get_window` center : bool - If `True`, the signal ``y`` is padded so that frame ``t`` is centered at ``y[t * hop_length]``. - If `False`, then frame ``t`` begins at ``y[t * hop_length]`` pad_mode : str If ``center=True``, the padding mode to use at the edges of the signal. By default, STFT uses zero padding. freq : None or np.ndarray [shape=(d,) or shape=(..., d, t)] Center frequencies for spectrogram bins. If `None`, then FFT bin center frequencies are used. Otherwise, it can be a single array of ``d`` center frequencies, .. note:: ``freq`` is assumed to be sorted in increasing order roll_percent : float [0 < roll_percent < 1] Roll-off percentage. Returns ------- rolloff : np.ndarray [shape=(..., 1, t)] roll-off frequency for each frame Examples -------- From time-series input >>> y, sr = librosa.loadx('trumpet') >>> # Approximate maximum frequencies with roll_percent=0.85 (default) >>> librosa.feature.spectral_rolloff(y=y, sr=sr) array([[2583.984, 3036.182, ..., 9173.145, 9248.511]]) >>> # Approximate maximum frequencies with roll_percent=0.99 >>> rolloff = librosa.feature.spectral_rolloff(y=y, sr=sr, roll_percent=0.99) >>> rolloff array([[ 7192.09 , 6739.893, ..., 10960.4 , 10992.7 ]]) >>> # Approximate minimum frequencies with roll_percent=0.01 >>> rolloff_min = librosa.feature.spectral_rolloff(y=y, sr=sr, roll_percent=0.01) >>> rolloff_min array([[516.797, 538.33 , ..., 764.429, 764.429]]) From spectrogram input >>> S, phase = librosa.magphase(librosa.stft(y)) >>> librosa.feature.spectral_rolloff(S=S, sr=sr) array([[2583.984, 3036.182, ..., 9173.145, 9248.511]]) >>> # With a higher roll percentage: >>> librosa.feature.spectral_rolloff(y=y, sr=sr, roll_percent=0.95) array([[ 3919.043, 3994.409, ..., 10443.604, 10594.336]]) >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots() >>> librosa.display.specshow(S, vscale='dBFS', ... y_axis='log', x_axis='time', ax=ax) >>> hl = librosa.display.highlight(ax=ax, linewidth=3, alpha=0.5) >>> ax.plot(librosa.times_like(rolloff), rolloff_min[0], ... path_effects=hl, ... label='Roll-off frequency (0.01)') >>> ax.plot(librosa.times_like(rolloff), rolloff[0], ... path_effects=hl, ... label='Roll-off frequency (0.99)') >>> ax.legend(loc='lower right') >>> ax.set(title='log Power spectrogram') """ if not 0.0 < roll_percent < 1.0: raise ParameterError("roll_percent must lie in the range (0, 1)") S, n_fft = _spectrogram( y=y, S=S, n_fft=n_fft, hop_length=hop_length, win_length=win_length, window=window, center=center, pad_mode=pad_mode, ) if not np.isrealobj(S): raise ParameterError( "Spectral rolloff is only defined " "with real-valued input" ) elif np.any(S < 0): raise ParameterError( "Spectral rolloff is only defined " "with non-negative energies" ) # Compute the center frequencies of each bin if freq is None: freq = fft_frequencies(sr=sr, n_fft=n_fft) # Make sure that frequency can be broadcast if freq.ndim == 1: # reshape for broadcasting freq = util.expand_to(freq, ndim=S.ndim, axes=-2) total_energy = np.cumsum(S, axis=-2) # (channels,freq,frames) threshold = roll_percent * total_energy[..., -1, :] # reshape threshold for broadcasting threshold = np.expand_dims(threshold, axis=-2) ind = np.where(total_energy < threshold, np.nan, 1) rolloff: np.ndarray = np.nanmin(ind * freq, axis=-2, keepdims=True) return rolloff def spectral_flatness( *, y: np.ndarray | None = None, S: np.ndarray | None = None, n_fft: int = 2048, hop_length: int = 512, win_length: int | None = None, window: _WindowSpec = "hann", center: bool = True, pad_mode: _PadModeSTFT = "constant", amin: float = 1e-10, power: float = 2.0, ) -> np.ndarray: """Compute spectral flatness Spectral flatness (or tonality coefficient) is a measure to quantify how much noise-like a sound is, as opposed to being tone-like [#]_. A high spectral flatness (closer to 1.0) indicates the spectrum is similar to white noise. It is often converted to decibel. .. [#] Dubnov, Shlomo "Generalization of spectral flatness measure for non-gaussian linear processes" IEEE Signal Processing Letters, 2004, Vol. 11. Parameters ---------- y : np.ndarray [shape=(..., n)] or None audio time series. Multi-channel is supported. S : np.ndarray [shape=(..., d, t)] or None (optional) pre-computed spectrogram magnitude n_fft : int > 0 [scalar] length of the FFT frame hop_length : int > 0 [scalar] hop length for STFT. See `librosa.stft` for details. win_length : int <= n_fft [scalar] Each frame of audio is windowed by `window()`. The window will be of length `win_length` and then padded with zeros to match ``n_fft``. If unspecified, defaults to ``win_length = n_fft``. window : str, tuple, number, function, or np.ndarray [shape=(n_fft,)] - a window specification (str, tuple, or number); see `scipy.signal.get_window` - a window function, such as `scipy.signal.windows.hann` - a vector or array of length ``n_fft`` .. see also:: `librosa.filters.get_window` center : bool - If `True`, the signal ``y`` is padded so that frame ``t`` is centered at ``y[t * hop_length]``. - If `False`, then frame `t` begins at ``y[t * hop_length]`` pad_mode : str If ``center=True``, the padding mode to use at the edges of the signal. By default, STFT uses zero padding. amin : float > 0 [scalar] minimum threshold for ``S`` (=added noise floor for numerical stability) power : float > 0 [scalar] Exponent for the magnitude spectrogram. e.g., 1 for energy, 2 for power, etc. Power spectrogram is usually used for computing spectral flatness. Returns ------- flatness : np.ndarray [shape=(..., 1, t)] spectral flatness for each frame. The returned value is in [0, 1] and often converted to dB scale. Examples -------- From time-series input >>> y, sr = librosa.loadx('trumpet') >>> flatness = librosa.feature.spectral_flatness(y=y) >>> flatness array([[0.001, 0. , ..., 0.218, 0.184]], dtype=float32) From spectrogram input >>> S, phase = librosa.magphase(librosa.stft(y)) >>> librosa.feature.spectral_flatness(S=S) array([[0.001, 0. , ..., 0.218, 0.184]], dtype=float32) From power spectrogram input >>> S, phase = librosa.magphase(librosa.stft(y)) >>> S_power = S ** 2 >>> librosa.feature.spectral_flatness(S=S_power, power=1.0) array([[0.001, 0. , ..., 0.218, 0.184]], dtype=float32) """ if amin <= 0: raise ParameterError("amin must be strictly positive") S, n_fft = _spectrogram( y=y, S=S, n_fft=n_fft, hop_length=hop_length, power=1.0, win_length=win_length, window=window, center=center, pad_mode=pad_mode, ) if not np.isrealobj(S): raise ParameterError( "Spectral flatness is only defined " "with real-valued input" ) elif np.any(S < 0): raise ParameterError( "Spectral flatness is only defined " "with non-negative energies" ) S_thresh = np.maximum(amin, S**power) gmean = np.exp(np.mean(np.log(S_thresh), axis=-2, keepdims=True)) amean = np.mean(S_thresh, axis=-2, keepdims=True) flatness: np.ndarray = gmean / amean return flatness def rms( *, y: np.ndarray | None = None, S: np.ndarray | None = None, frame_length: int = 2048, hop_length: int = 512, center: bool = True, pad_mode: _PadMode = "constant", dtype: DTypeLike = np.float32, ) -> np.ndarray: """Compute root-mean-square (RMS) value for each frame. Input can be given either as a time series ``y`` or as a spectrogram magnitude ``S``. Computing the RMS value from audio samples is faster as it doesn't require a STFT calculation. However, using a spectrogram will give a more accurate representation of energy over time because its frames can be windowed, thus prefer using ``S`` if it's already available. Parameters ---------- y : np.ndarray [shape=(..., n)] or None (optional) audio time series. Required if ``S`` is not input. Multi-channel is supported. S : np.ndarray [shape=(..., d, t)] or None (optional) spectrogram magnitude. Required if ``y`` is not input. frame_length : int > 0 [scalar] length of analysis frame (in samples) for energy calculation hop_length : int > 0 [scalar] hop length for STFT. See `librosa.stft` for details. center : bool If `True` and operating on time-domain input (``y``), pad the signal by ``frame_length//2`` on either side. If operating on spectrogram input, this has no effect. pad_mode : str Padding mode for centered analysis. See `numpy.pad` for valid values. dtype : np.dtype, optional Data type of the output array. Defaults to float32. Returns ------- rms : np.ndarray [shape=(..., 1, t)] RMS value for each frame Examples -------- >>> y, sr = librosa.loadx('trumpet') >>> librosa.feature.rms(y=y) array([[1.248e-01, 1.259e-01, ..., 1.845e-05, 1.796e-05]], dtype=float32) Or from spectrogram input >>> S, phase = librosa.magphase(librosa.stft(y)) >>> rms = librosa.feature.rms(S=S) >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=2, sharex=True) >>> times = librosa.times_like(rms) >>> ax[0].semilogy(times, rms[0], label='RMS Energy') >>> ax[0].set(xticks=[]) >>> ax[0].legend() >>> ax[0].label_outer() >>> librosa.display.specshow(S, vscale='dBFS', ... y_axis='log', x_axis='time', ax=ax[1]) >>> ax[1].set(title='log Power spectrogram') Use a STFT window of constant ones and no frame centering to get consistent results with the RMS computed from the audio samples ``y`` >>> S = librosa.magphase(librosa.stft(y, window=np.ones, center=False))[0] >>> librosa.feature.rms(S=S) >>> plt.show() """ if y is not None: util.valid_audio(y) if center: padding = [(0, 0) for _ in range(y.ndim)] padding[-1] = (int(frame_length // 2), int(frame_length // 2)) y = np.pad(y, padding, mode=pad_mode) x = util.frame(y, frame_length=frame_length, hop_length=hop_length) # Calculate power power = np.mean(util.abs2(x, dtype=dtype), axis=-2, keepdims=True) elif S is not None: # Check the frame length if S.shape[-2] != frame_length // 2 + 1: raise ParameterError( "Since S.shape[-2] is {}, " "frame_length is expected to be {} or {}; " "found {}".format( S.shape[-2], S.shape[-2] * 2 - 2, S.shape[-2] * 2 - 1, frame_length ) ) # power spectrogram x = util.abs2(S, dtype=dtype) # Adjust the DC and sr/2 component x[..., 0, :] *= 0.5 if frame_length % 2 == 0: x[..., -1, :] *= 0.5 # Calculate power power = 2 * np.sum(x, axis=-2, keepdims=True) / frame_length**2 else: raise ParameterError("Either `y` or `S` must be input.") rms_result: np.ndarray = np.sqrt(power) return rms_result def poly_features( *, y: np.ndarray | None = None, sr: float = 22050, S: np.ndarray | None = None, n_fft: int = 2048, hop_length: int = 512, win_length: int | None = None, window: _WindowSpec = "hann", center: bool = True, pad_mode: _PadModeSTFT = "constant", order: int = 1, freq: np.ndarray | None = None, ) -> np.ndarray: """Get coefficients of fitting an nth-order polynomial to the columns of a spectrogram. Parameters ---------- y : np.ndarray [shape=(..., n)] or None audio time series. Multi-channel is supported. sr : number > 0 [scalar] audio sampling rate of ``y`` S : np.ndarray [shape=(..., d, t)] or None (optional) spectrogram magnitude n_fft : int > 0 [scalar] length of the FFT frame hop_length : int > 0 [scalar] hop length for STFT. See `librosa.stft` for details. win_length : int <= n_fft [scalar] Each frame of audio is windowed by `window()`. The window will be of length `win_length` and then padded with zeros to match ``n_fft``. If unspecified, defaults to ``win_length = n_fft``. window : str, tuple, number, function, or np.ndarray [shape=(n_fft,)] - a window specification (str, tuple, or number); see `scipy.signal.get_window` - a window function, such as `scipy.signal.windows.hann` - a vector or array of length ``n_fft`` .. see also:: `librosa.filters.get_window` center : bool - If `True`, the signal ``y`` is padded so that frame `t` is centered at ``y[t * hop_length]``. - If `False`, then frame ``t`` begins at ``y[t * hop_length]`` pad_mode : str If ``center=True``, the padding mode to use at the edges of the signal. By default, STFT uses zero padding. order : int > 0 order of the polynomial to fit freq : None or np.ndarray [shape=(d,) or shape=(..., d, t)] Center frequencies for spectrogram bins. If `None`, then FFT bin center frequencies are used. Otherwise, it can be a single array of ``d`` center frequencies, or a matrix of center frequencies as constructed by `librosa.reassigned_spectrogram` Returns ------- coefficients : np.ndarray [shape=(..., order+1, t)] polynomial coefficients for each frame. ``coefficients[..., 0, :]`` corresponds to the highest degree (``order``), ``coefficients[..., 1, :]`` corresponds to the next highest degree (``order-1``), down to the constant term ``coefficients[..., order, :]``. Examples -------- >>> y, sr = librosa.loadx('trumpet') >>> S = np.abs(librosa.stft(y)) Fit a degree-0 polynomial (constant) to each frame >>> p0 = librosa.feature.poly_features(S=S, order=0) Fit a linear polynomial to each frame >>> p1 = librosa.feature.poly_features(S=S, order=1) Fit a quadratic to each frame >>> p2 = librosa.feature.poly_features(S=S, order=2) Plot the results for comparison >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=4, sharex=True, figsize=(8, 8)) >>> times = librosa.times_like(p0) >>> ax[0].plot(times, p0[0], label='order=0', alpha=0.8) >>> ax[0].plot(times, p1[1], label='order=1', alpha=0.8) >>> ax[0].plot(times, p2[2], label='order=2', alpha=0.8) >>> ax[0].legend() >>> ax[0].label_outer() >>> ax[0].set(ylabel='Constant term ') >>> ax[1].plot(times, p1[0], label='order=1', alpha=0.8) >>> ax[1].plot(times, p2[1], label='order=2', alpha=0.8) >>> ax[1].set(ylabel='Linear term') >>> ax[1].label_outer() >>> ax[1].legend() >>> ax[2].plot(times, p2[0], label='order=2', alpha=0.8) >>> ax[2].set(ylabel='Quadratic term') >>> ax[2].legend() >>> librosa.display.specshow(S, vscale='dBFS', ... y_axis='log', x_axis='time', ax=ax[3]) """ S, n_fft = _spectrogram( y=y, S=S, n_fft=n_fft, hop_length=hop_length, win_length=win_length, window=window, center=center, pad_mode=pad_mode, ) # Compute the center frequencies of each bin if freq is None: freq = fft_frequencies(sr=sr, n_fft=n_fft) coefficients: np.ndarray if freq.ndim == 1: # If frequencies are constant over frames, then we only need to fit once fitter = np.vectorize( lambda y: np.polyfit(freq, y, order), signature="(f,t)->(d,t)" ) coefficients = fitter(S) else: # Otherwise, we have variable frequencies, and need to fit independently fitter = np.vectorize( lambda x, y: np.polyfit(x, y, order), signature="(f),(f)->(d)" ) # We have to do some axis swapping to preserve layout # otherwise, the new dimension gets put at the end instead of the penultimate position coefficients = fitter(freq.swapaxes(-2, -1), S.swapaxes(-2, -1)).swapaxes( -2, -1 ) return coefficients def zero_crossing_rate( y: np.ndarray, *, frame_length: int = 2048, hop_length: int = 512, center: bool = True, **kwargs: Any, ) -> np.ndarray: """Compute the zero-crossing rate of an audio time series. Parameters ---------- y : np.ndarray [shape=(..., n)] Audio time series. Multi-channel is supported. frame_length : int > 0 Length of the frame over which to compute zero crossing rates hop_length : int > 0 Number of samples to advance for each frame center : bool If `True`, frames are centered by padding the edges of ``y``. This is similar to the padding in `librosa.stft`, but uses edge-value copies instead of zero-padding. **kwargs additional keyword arguments to pass to `librosa.zero_crossings` threshold : float >= 0 If specified, values where ``-threshold <= y <= threshold`` are clipped to 0. ref_magnitude : float > 0 or callable If numeric, the threshold is scaled relative to ``ref_magnitude``. If callable, the threshold is scaled relative to ``ref_magnitude(np.abs(y))``. zero_pos : bool If ``True`` then the value 0 is interpreted as having positive sign. If ``False``, then 0, -1, and +1 all have distinct signs. axis : int Axis along which to compute zero-crossings. .. note:: By default, the ``pad`` parameter is set to `False`, which differs from the default specified by `librosa.zero_crossings`. Returns ------- zcr : np.ndarray [shape=(..., 1, t)] ``zcr[..., 0, i]`` is the fraction of zero crossings in frame ``i`` See Also -------- librosa.zero_crossings : Compute zero-crossings in a time-series Examples -------- >>> y, sr = librosa.loadx('trumpet') >>> librosa.feature.zero_crossing_rate(y) array([[0.044, 0.074, ..., 0.488, 0.355]]) """ # check if audio is valid util.valid_audio(y) if center: padding = [(0, 0) for _ in range(y.ndim)] padding[-1] = (int(frame_length // 2), int(frame_length // 2)) y = np.pad(y, padding, mode="edge") y_framed = util.frame(y, frame_length=frame_length, hop_length=hop_length) kwargs["axis"] = -2 kwargs.setdefault("pad", False) crossings = zero_crossings(y_framed, **kwargs) zcrate: np.ndarray = np.mean(crossings, axis=-2, keepdims=True) return zcrate # -- Chroma --# def chroma_stft( *, y: np.ndarray | None = None, sr: float = 22050, S: np.ndarray | None = None, norm: float | None = np.inf, n_fft: int = 2048, hop_length: int = 512, win_length: int | None = None, window: _WindowSpec = "hann", center: bool = True, pad_mode: _PadModeSTFT = "constant", tuning: float | None = None, n_chroma: int = 12, **kwargs: Any, ) -> np.ndarray: """Compute a chromagram from a waveform or power spectrogram. This implementation is derived from ``chromagram_E`` [#]_ .. [#] Ellis, Daniel P.W. "Chroma feature analysis and synthesis" 2007/04/21 https://www.ee.columbia.edu/~dpwe/resources/matlab/chroma-ansyn/ Parameters ---------- y : np.ndarray [shape=(..., n)] or None audio time series. Multi-channel is supported. sr : number > 0 [scalar] sampling rate of ``y`` S : np.ndarray [shape=(..., d, t)] or None power spectrogram norm : float or None Column-wise normalization. See `librosa.util.normalize` for details. If `None`, no normalization is performed. n_fft : int > 0 [scalar] length of the FFT frame, if ``y, sr`` are provided instead of ``S`` hop_length : int > 0 [scalar] hop length if provided ``y, sr`` instead of ``S`` win_length : int <= n_fft [scalar] Each frame of audio is windowed by `window()`. The window will be of length `win_length` and then padded with zeros to match ``n_fft``. If unspecified, defaults to ``win_length = n_fft``. window : str, tuple, number, function, or np.ndarray [shape=(n_fft,)] - a window specification (str, tuple, or number); see `scipy.signal.get_window` - a window function, such as `scipy.signal.windows.hann` - a vector or array of length ``n_fft`` .. see also:: `librosa.filters.get_window` center : bool - If `True`, the signal ``y`` is padded so that frame ``t`` is centered at ``y[t * hop_length]``. - If `False`, then frame ``t`` begins at ``y[t * hop_length]`` pad_mode : str If ``center=True``, the padding mode to use at the edges of the signal. By default, STFT uses zero padding. tuning : float [scalar] or None Deviation from A440 tuning in fractional chroma bins. If `None`, it is automatically estimated. n_chroma : int > 0 [scalar] Number of chroma bins to produce (12 by default). **kwargs additional keyword arguments to parameterize chroma filters. ctroct : float > 0 [scalar] See `octwidth` octwidth : float > 0 or None [scalar] ``ctroct`` and ``octwidth`` specify a dominance window: a Gaussian weighting centered on ``ctroct`` (in octs, A0 = 27.5Hz) and with a gaussian half-width of ``octwidth``. Set ``octwidth`` to `None` to use a flat weighting. norm : float > 0 or np.inf Normalization factor for each filter base_c : bool If True, the filter bank will start at 'C'. If False, the filter bank will start at 'A'. dtype : np.dtype The data type of the output basis. By default, uses 32-bit (single-precision) floating point. Returns ------- chromagram : np.ndarray [shape=(..., n_chroma, t)] Normalized energy for each chroma bin at each frame. See Also -------- librosa.filters.chroma : Chroma filter bank construction librosa.util.normalize : Vector normalization Examples -------- >>> y, sr = librosa.loadx('nutcracker', duration=15) >>> librosa.feature.chroma_stft(y=y, sr=sr) array([[1. , 0.962, ..., 0.143, 0.278], [0.688, 0.745, ..., 0.103, 0.162], ..., [0.468, 0.598, ..., 0.18 , 0.342], [0.681, 0.702, ..., 0.553, 1. ]], dtype=float32) Use an energy (magnitude) spectrum instead of power spectrogram >>> S = np.abs(librosa.stft(y)) >>> chroma = librosa.feature.chroma_stft(S=S, sr=sr) >>> chroma array([[1. , 0.973, ..., 0.527, 0.569], [0.774, 0.81 , ..., 0.518, 0.506], ..., [0.624, 0.73 , ..., 0.611, 0.644], [0.766, 0.822, ..., 0.92 , 1. ]], dtype=float32) Use a pre-computed power spectrogram with a larger frame >>> S = np.abs(librosa.stft(y, n_fft=4096))**2 >>> chroma = librosa.feature.chroma_stft(S=S, sr=sr) >>> chroma array([[0.994, 0.873, ..., 0.169, 0.227], [0.735, 0.64 , ..., 0.141, 0.135], ..., [0.6 , 0.937, ..., 0.214, 0.257], [0.743, 0.937, ..., 0.684, 0.815]], dtype=float32) >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=2, sharex=True) >>> img = librosa.display.specshow(S, vscale='dBFS[power]', ... y_axis='log', x_axis='time', ax=ax[0]) >>> librosa.display.colorbar_db(img) >>> ax[0].label_outer() >>> img = librosa.display.specshow(chroma, y_axis='chroma', x_axis='time', ax=ax[1]) >>> fig.colorbar(img, ax=[ax[1]]) """ S, n_fft = _spectrogram( y=y, S=S, n_fft=n_fft, hop_length=hop_length, power=2, win_length=win_length, window=window, center=center, pad_mode=pad_mode, ) if tuning is None: tuning = estimate_tuning(S=S, sr=sr, bins_per_octave=n_chroma) # Get the filter bank chromafb = filters.chroma( sr=sr, n_fft=n_fft, tuning=tuning, n_chroma=n_chroma, **kwargs ) # Compute raw chroma raw_chroma = np.einsum("cf,...ft->...ct", chromafb, S, optimize=True) # Compute normalization factor for each frame return util.normalize(raw_chroma, norm=norm, axis=-2) def chroma_cqt( *, y: np.ndarray | None = None, sr: float = 22050, C: np.ndarray | None = None, hop_length: int = 512, fmin: _FloatLike_co | None = None, norm: float | None = np.inf, threshold: float = 0.0, tuning: float | None = None, n_chroma: int = 12, n_octaves: int = 7, window: np.ndarray | None = None, bins_per_octave: int | None = 36, cqt_mode: str = "full", ) -> np.ndarray: r"""Constant-Q chromagram Parameters ---------- y : np.ndarray [shape=(..., n,)] audio time series. Multi-channel is supported. sr : number > 0 sampling rate of ``y`` C : np.ndarray [shape=(..., d, t)] [Optional] a pre-computed constant-Q spectrogram hop_length : int > 0 number of samples between successive chroma frames fmin : float > 0 minimum frequency to analyze in the CQT. Default: `C1 ~= 32.7 Hz` norm : int > 0, +-np.inf, or None Column-wise normalization of the chromagram. threshold : float Pre-normalization energy threshold. Values below the threshold are discarded, resulting in a sparse chromagram. tuning : float [scalar] or None Deviation (in fractions of a CQT bin) from A440 tuning n_chroma : int > 0 Number of chroma bins to produce n_octaves : int > 0 Number of octaves to analyze above ``fmin`` window : None or np.ndarray Optional window parameter to `filters.cq_to_chroma` Must be an integer multiple of ``n_chroma``. Default: 36 (3 bins per semitone) If `None`, it will match ``n_chroma``. bins_per_octave : int > 0, optional Number of bins per octave in the CQT. cqt_mode : ['full', 'hybrid'] Constant-Q transform mode Returns ------- chromagram : np.ndarray [shape=(..., n_chroma, t)] The output chromagram See Also -------- librosa.util.normalize librosa.cqt librosa.hybrid_cqt chroma_stft Examples -------- Compare a long-window STFT chromagram to the CQT chromagram >>> y, sr = librosa.loadx('nutcracker', duration=15) >>> chroma_stft = librosa.feature.chroma_stft(y=y, sr=sr, ... n_chroma=12, n_fft=4096) >>> chroma_cq = librosa.feature.chroma_cqt(y=y, sr=sr) >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=2, sharex=True, sharey=True) >>> librosa.display.specshow(chroma_stft, y_axis='chroma', x_axis='time', ax=ax[0]) >>> ax[0].set(title='chroma_stft') >>> ax[0].label_outer() >>> img = librosa.display.specshow(chroma_cq, y_axis='chroma', x_axis='time', ax=ax[1]) >>> ax[1].set(title='chroma_cqt') >>> fig.colorbar(img, ax=ax) """ cqt_func = {"full": cqt, "hybrid": hybrid_cqt} if bins_per_octave is None: bins_per_octave = n_chroma elif np.remainder(bins_per_octave, n_chroma) != 0: raise ParameterError( f"bins_per_octave={bins_per_octave} must be an integer " f"multiple of n_chroma={n_chroma}" ) # Build the CQT if we don't have one already if C is None: if y is None: raise ParameterError( "At least one of C or y must be provided to compute chroma" ) C = np.abs( cqt_func[cqt_mode]( y, sr=sr, hop_length=hop_length, fmin=fmin, n_bins=n_octaves * bins_per_octave, bins_per_octave=bins_per_octave, tuning=tuning, ) ) # Map to chroma cq_to_chr = filters.cq_to_chroma( C.shape[-2], bins_per_octave=bins_per_octave, n_chroma=n_chroma, fmin=fmin, window=window, ) chroma = np.einsum("cf,...ft->...ct", cq_to_chr, C, optimize=True) if threshold is not None: chroma[chroma < threshold] = 0.0 # Normalize chroma = util.normalize(chroma, norm=norm, axis=-2) return chroma def chroma_cens( *, y: np.ndarray | None = None, sr: float = 22050, C: np.ndarray | None = None, hop_length: int = 512, fmin: _FloatLike_co | None = None, tuning: float | None = None, n_chroma: int = 12, n_octaves: int = 7, bins_per_octave: int = 36, cqt_mode: str = "full", window: np.ndarray | None = None, norm: float | None = 2, win_len_smooth: int | None = 41, smoothing_window: _WindowSpec = "hann", ) -> np.ndarray: r"""Compute the chroma variant "Chroma Energy Normalized" (CENS) To compute CENS features, following steps are taken after obtaining chroma vectors using `chroma_cqt`: [#]_. 1. L-1 normalization of each chroma vector 2. Quantization of amplitude based on "log-like" amplitude thresholds 3. (optional) Smoothing with sliding window. Default window length = 41 frames 4. (not implemented) Downsampling CENS features are robust to dynamics, timbre and articulation, thus these are commonly used in audio matching and retrieval applications. .. [#] Meinard Müller and Sebastian Ewert "Chroma Toolbox: MATLAB implementations for extracting variants of chroma-based audio features" In Proceedings of the International Conference on Music Information Retrieval (ISMIR), 2011. Parameters ---------- y : np.ndarray [shape=(..., n,)] audio time series. Multi-channel is supported. sr : number > 0 sampling rate of ``y`` C : np.ndarray [shape=(d, t)] [Optional] a pre-computed constant-Q spectrogram hop_length : int > 0 number of samples between successive chroma frames fmin : float > 0 minimum frequency to analyze in the CQT. Default: `C1 ~= 32.7 Hz` tuning : float [scalar] or None Deviation (in fractions of a CQT bin) from A440 tuning n_chroma : int > 0 Number of chroma bins to produce n_octaves : int > 0 Number of octaves to analyze above ``fmin`` bins_per_octave : int > 0 Number of bins per octave in the CQT. Default: 36 cqt_mode : ['full', 'hybrid'] Constant-Q transform mode window : None or np.ndarray Optional window parameter to `filters.cq_to_chroma` norm : int > 0, +-np.inf, or None Column-wise normalization of the chromagram. win_len_smooth : int > 0 or None Length of temporal smoothing window. `None` disables temporal smoothing. Default: 41 smoothing_window : str, float or tuple Type of window function for temporal smoothing. See `librosa.filters.get_window` for possible inputs. Default: 'hann' Returns ------- cens : np.ndarray [shape=(..., n_chroma, t)] The output cens-chromagram See Also -------- chroma_cqt : Compute a chromagram from a constant-Q transform. chroma_stft : Compute a chromagram from an STFT spectrogram or waveform. librosa.filters.get_window : Compute a window function. Examples -------- Compare standard cqt chroma to CENS. >>> y, sr = librosa.loadx('nutcracker', duration=15) >>> chroma_cens = librosa.feature.chroma_cens(y=y, sr=sr) >>> chroma_cq = librosa.feature.chroma_cqt(y=y, sr=sr) >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=2, sharex=True, sharey=True) >>> img = librosa.display.specshow(chroma_cq, y_axis='chroma', x_axis='time', ax=ax[0]) >>> ax[0].set(title='chroma_cq') >>> ax[0].label_outer() >>> librosa.display.specshow(chroma_cens, y_axis='chroma', x_axis='time', ax=ax[1]) >>> ax[1].set(title='chroma_cens') >>> fig.colorbar(img, ax=ax) """ if not ( (win_len_smooth is None) or (isinstance(win_len_smooth, (int, np.integer)) and win_len_smooth > 0) ): raise ParameterError( f"win_len_smooth={win_len_smooth} must be a positive integer or None" ) chroma = chroma_cqt( y=y, C=C, sr=sr, hop_length=hop_length, fmin=fmin, bins_per_octave=bins_per_octave, tuning=tuning, norm=None, n_chroma=n_chroma, n_octaves=n_octaves, cqt_mode=cqt_mode, window=window, ) # L1-Normalization chroma = util.normalize(chroma, norm=1, axis=-2) # Quantize amplitudes QUANT_STEPS = [0.4, 0.2, 0.1, 0.05] QUANT_WEIGHTS = [0.25, 0.25, 0.25, 0.25] chroma_quant = np.zeros_like(chroma) for cur_quant_step_idx, cur_quant_step in enumerate(QUANT_STEPS): chroma_quant += (chroma > cur_quant_step) * QUANT_WEIGHTS[cur_quant_step_idx] if win_len_smooth: # Apply temporal smoothing win = filters.get_window(smoothing_window, win_len_smooth + 2, fftbins=False) win /= np.sum(win) # reshape for broadcasting win = util.expand_to(win, ndim=chroma_quant.ndim, axes=-1) import scipy.ndimage cens = scipy.ndimage.convolve(chroma_quant, win, mode="constant") else: cens = chroma_quant # L2-Normalization return util.normalize(cens, norm=norm, axis=-2) def chroma_vqt( *, y: np.ndarray | None = None, sr: float = 22050, V: np.ndarray | None = None, hop_length: int = 512, fmin: float | None = None, intervals: str | Collection[float], norm: float | None = np.inf, threshold: float = 0.0, n_octaves: int = 7, bins_per_octave: int = 12, gamma: float = 0, ) -> np.ndarray: r"""Variable-Q chromagram This differs from CQT-based chroma by supporting non-equal temperament intervals. Note: unlike CQT- and STFT-based chroma, VQT chroma does not aggregate energy from neighboring frequency bands. As a result, the number of chroma features produced is equal to the number of intervals used, or equivalently, the number of bins per octave in the underlying VQT representation. Parameters ---------- y : np.ndarray [shape=(..., n,)] audio time series. Multi-channel is supported. sr : number > 0 sampling rate of ``y`` V : np.ndarray [shape=(..., d, t)] [Optional] a pre-computed variable-Q spectrogram hop_length : int > 0 number of samples between successive chroma frames fmin : float > 0 minimum frequency to analyze in the CQT. Default: `C1 ~= 32.7 Hz` intervals : str or array of floats in [1, 2) Either a str specification for an interval set, e.g., `'equal'`, `'pythagorean'`, `'ji3'`, etc. or an array of intervals expressed as numbers between 1 and 2. .. see also:: librosa.interval_frequencies norm : int > 0, +-np.inf, or None Column-wise normalization of the chromagram. threshold : float Pre-normalization energy threshold. Values below the threshold are discarded, resulting in a sparse chromagram. n_octaves : int > 0 Number of octaves to analyze above ``fmin`` bins_per_octave : int > 0, optional Number of bins per octave in the VQT. gamma : number > 0 [scalar] Bandwidth offset for determining filter lengths. .. see also:: librosa.vqt Returns ------- chromagram : np.ndarray [shape=(..., bins_per_octave, t)] The output chromagram See Also -------- librosa.util.normalize librosa.vqt chroma_cqt librosa.interval_frequencies Examples -------- Compare an equal-temperament CQT chromagram to a 5-limit just intonation chromagram. Both use 36 bins per octave. >>> y, sr = librosa.loadx('trumpet') >>> n_bins = 36 >>> chroma_cq = librosa.feature.chroma_cqt(y=y, sr=sr, n_chroma=n_bins) >>> chroma_vq = librosa.feature.chroma_vqt(y=y, sr=sr, ... intervals='ji5', ... bins_per_octave=n_bins) >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=2, sharex=True) >>> librosa.display.specshow(chroma_cq, y_axis='chroma', x_axis='time', ... ax=ax[0], bins_per_octave=n_bins) >>> ax[0].set(ylabel='chroma_cqt') >>> ax[0].label_outer() >>> img = librosa.display.specshow(chroma_vq, y_axis='chroma_fjs', x_axis='time', ... ax=ax[1], bins_per_octave=n_bins, ... intervals='ji5') >>> ax[1].set(ylabel='chroma_vqt') >>> fig.colorbar(img, ax=ax) """ # If intervals are provided as an array, override BPO if not isinstance(intervals, str): bins_per_octave = len(intervals) # Build the CQT if we don't have one already if V is None: if y is None: raise ParameterError( "At least one of y or V must be provided to compute chroma" ) V = np.abs( vqt( y=y, sr=sr, hop_length=hop_length, fmin=fmin, intervals=intervals, n_bins=n_octaves * bins_per_octave, bins_per_octave=bins_per_octave, gamma=gamma, ) ) # Map to chroma vq_to_chr = filters.cq_to_chroma( V.shape[-2], bins_per_octave=bins_per_octave, n_chroma=bins_per_octave, fmin=fmin, ) chroma = np.einsum("cf,...ft->...ct", vq_to_chr, V, optimize=True) if threshold is not None: chroma[chroma < threshold] = 0.0 # Normalize chroma = util.normalize(chroma, norm=norm, axis=-2) return chroma def tonnetz( *, y: np.ndarray | None = None, sr: float = 22050, chroma: np.ndarray | None = None, **kwargs: Any, ) -> np.ndarray: """Compute the tonal centroid features (tonnetz) This representation uses the method of [#]_ to project chroma features onto a 6-dimensional basis representing the perfect fifth, minor third, and major third each as two-dimensional coordinates. .. [#] Harte, C., Sandler, M., & Gasser, M. (2006). "Detecting Harmonic Change in Musical Audio." In Proceedings of the 1st ACM Workshop on Audio and Music Computing Multimedia (pp. 21-26). Santa Barbara, CA, USA: ACM Press. doi:10.1145/1178723.1178727. Parameters ---------- y : np.ndarray [shape=(..., n,)] or None Audio time series. Multi-channel is supported. sr : number > 0 [scalar] sampling rate of ``y`` chroma : np.ndarray [shape=(n_chroma, t)] or None Normalized energy for each chroma bin at each frame. If `None`, a cqt chromagram is performed. **kwargs : Additional keyword arguments to `chroma_cqt`, if ``chroma`` is not pre-computed. C : np.ndarray [shape=(..., d, t)] [Optional] a pre-computed constant-Q spectrogram hop_length : int > 0 number of samples between successive chroma frames fmin : float > 0 minimum frequency to analyze in the CQT. Default: `C1 ~= 32.7 Hz` norm : int > 0, +-np.inf, or None Column-wise normalization of the chromagram. threshold : float Pre-normalization energy threshold. Values below the threshold are discarded, resulting in a sparse chromagram. tuning : float [scalar] or None Deviation (in fractions of a CQT bin) from A440 tuning n_chroma : int > 0 Number of chroma bins to produce n_octaves : int > 0 Number of octaves to analyze above ``fmin`` window : None or np.ndarray Optional window parameter to `filters.cq_to_chroma` bins_per_octave : int > 0, optional Number of bins per octave in the CQT. Must be an integer multiple of ``n_chroma``. Default: 36 (3 bins per semitone) If `None`, it will match ``n_chroma``. cqt_mode : ['full', 'hybrid'] Constant-Q transform mode Returns ------- tonnetz : np.ndarray [shape(..., 6, t)] Tonal centroid features for each frame. Tonnetz dimensions: - 0: Fifth x-axis - 1: Fifth y-axis - 2: Minor x-axis - 3: Minor y-axis - 4: Major x-axis - 5: Major y-axis See Also -------- chroma_cqt : Compute a chromagram from a constant-Q transform. chroma_stft : Compute a chromagram from an STFT spectrogram or waveform. Examples -------- Compute tonnetz features from the harmonic component of a song >>> y, sr = librosa.loadx('nutcracker', duration=10, offset=10) >>> y = librosa.effects.harmonic(y) >>> tonnetz = librosa.feature.tonnetz(y=y, sr=sr) >>> tonnetz array([[ 0.007, -0.026, ..., 0.055, 0.056], [-0.01 , -0.009, ..., -0.012, -0.017], ..., [ 0.006, -0.021, ..., -0.012, -0.01 ], [-0.009, 0.031, ..., -0.05 , -0.037]]) Compare the tonnetz features to `chroma_cqt` >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=2, sharex=True) >>> img1 = librosa.display.specshow(tonnetz, ... y_axis='tonnetz', x_axis='time', ax=ax[0]) >>> ax[0].set(title='Tonal Centroids (Tonnetz)') >>> ax[0].label_outer() >>> img2 = librosa.display.specshow(librosa.feature.chroma_cqt(y=y, sr=sr), ... y_axis='chroma', x_axis='time', ax=ax[1]) >>> ax[1].set(title='Chroma') >>> fig.colorbar(img1, ax=[ax[0]]) >>> fig.colorbar(img2, ax=[ax[1]]) """ if y is None and chroma is None: raise ParameterError( "Either the audio samples or the chromagram must be " "passed as an argument." ) if chroma is None: chroma = chroma_cqt(y=y, sr=sr, **kwargs) # Generate Transformation matrix dim_map = np.linspace(0, 12, num=chroma.shape[-2], endpoint=False) scale = np.asarray([7.0 / 6, 7.0 / 6, 3.0 / 2, 3.0 / 2, 2.0 / 3, 2.0 / 3]) V = np.multiply.outer(scale, dim_map) # Even rows compute sin() V[::2] -= 0.5 R = np.array([1, 1, 1, 1, 0.5, 0.5]) # Fifths # Minor # Major phi = R[:, np.newaxis] * np.cos(np.pi * V) # Do the transform to tonnetz ton: np.ndarray = np.einsum( "pc,...ci->...pi", phi, util.normalize(chroma, norm=1, axis=-2), optimize=True ) return ton # -- Mel spectrogram and MFCCs -- # def mfcc( *, y: np.ndarray | None = None, sr: float = 22050, S: np.ndarray | None = None, n_mfcc: int = 20, dct_type: _DCTType = 2, norm: _DCTNorm | None = "ortho", lifter: float = 0, mel_norm: Literal["slaney"] | float | None = "slaney", **kwargs: Any, ) -> np.ndarray: """Mel-frequency cepstral coefficients (MFCCs) Parameters ---------- y : np.ndarray [shape=(..., n,)] or None audio time series. Multi-channel is supported.. sr : number > 0 [scalar] sampling rate of ``y`` S : np.ndarray [shape=(..., d, t)] or None log-power Mel spectrogram n_mfcc : int > 0 [scalar] number of MFCCs to return dct_type : {1, 2, 3} Discrete cosine transform (DCT) type. By default, DCT type-2 is used. norm : None or 'ortho' If ``dct_type`` is `2 or 3`, setting ``norm='ortho'`` uses an ortho-normal DCT basis. Normalization is not supported for ``dct_type=1``. lifter : number >= 0 If ``lifter>0``, apply *liftering* (cepstral filtering) to the MFCCs:: M[n, :] <- M[n, :] * (1 + sin(pi * (n + 1) / lifter) * lifter / 2) Setting ``lifter >= 2 * n_mfcc`` emphasizes the higher-order coefficients. As ``lifter`` increases, the coefficient weighting becomes approximately linear. mel_norm : float, 'slaney', or None `norm` argument to `melspectrogram` **kwargs additional keyword arguments to `melspectrogram` if operating on time series input n_fft : int > 0 [scalar] length of the FFT frame hop_length : int > 0 [scalar] number of samples between successive frames. See `librosa.stft` win_length : int <= n_fft [scalar] Each frame of audio is windowed by `window()`. The window will be of length `win_length` and then padded with zeros to match ``n_fft``. If unspecified, defaults to ``win_length = n_fft``. window : str, tuple, number, function, or np.ndarray [shape=(n_fft,)] - a window specification (str, tuple, or number); see `scipy.signal.get_window` - a window function, such as `scipy.signal.windows.hann` - a vector or array of length ``n_fft`` .. see also:: `librosa.filters.get_window` center : bool - If `True`, the signal ``y`` is padded so that frame ``t`` is centered at ``y[t * hop_length]``. - If `False`, then frame ``t`` begins at ``y[t * hop_length]`` pad_mode : str If ``center=True``, the padding mode to use at the edges of the signal. By default, STFT uses zero padding. power : float > 0 [scalar] Exponent applied to the spectrum before calculating the melspectrogram when the input is a time signal, e.g. 1 for magnitude, 2 for power **(default)**, etc. **kwargs additional keyword arguments for Mel filter bank parameters n_mels : int > 0 [scalar] number of Mel bands to generate fmin : float >= 0 [scalar] lowest frequency (in Hz) fmax : float >= 0 [scalar] highest frequency (in Hz). If `None`, use ``fmax = sr / 2.0`` htk : bool [scalar] use HTK formula instead of Slaney dtype : np.dtype The data type of the output basis. By default, uses 32-bit (single-precision) floating point. Returns ------- M : np.ndarray [shape=(..., n_mfcc, t)] MFCC sequence See Also -------- melspectrogram scipy.fft.dct Examples -------- Generate mfccs from a time series >>> y, sr = librosa.loadx('libri1') >>> librosa.feature.mfcc(y=y, sr=sr) array([[-565.919, -564.288, ..., -426.484, -434.668], [ 10.305, 12.509, ..., 88.43 , 90.12 ], ..., [ 2.807, 2.068, ..., -6.725, -5.159], [ 2.822, 2.244, ..., -6.198, -6.177]], dtype=float32) Using a different hop length and HTK-style Mel frequencies >>> librosa.feature.mfcc(y=y, sr=sr, hop_length=1024, htk=True) array([[-5.471e+02, -5.464e+02, ..., -4.446e+02, -4.200e+02], [ 1.361e+01, 1.402e+01, ..., 9.764e+01, 9.869e+01], ..., [ 4.097e-01, -2.029e+00, ..., -1.051e+01, -1.130e+01], [-1.119e-01, -1.688e+00, ..., -3.442e+00, -4.687e+00]], dtype=float32) Use a pre-computed log-power Mel spectrogram >>> S = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=128, ... fmax=8000) >>> librosa.feature.mfcc(S=librosa.power_to_db(S)) array([[-559.974, -558.449, ..., -411.96 , -420.458], [ 11.018, 13.046, ..., 76.972, 80.888], ..., [ 2.713, 2.379, ..., 1.464, -2.835], [ 2.712, 2.619, ..., 2.209, 0.648]], dtype=float32) Get more components >>> mfccs = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=40) Visualize the MFCC series >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=2, sharex=True) >>> img = librosa.display.specshow(S, vscale='dBFS[power]', ... x_axis='time', y_axis='mel', fmax=8000, ... ax=ax[0]) >>> librosa.display.colorbar_db(img) >>> ax[0].set(title='Mel spectrogram') >>> ax[0].label_outer() >>> img = librosa.display.specshow(mfccs, x_axis='time', ax=ax[1]) >>> fig.colorbar(img, ax=[ax[1]]) >>> ax[1].set(title='MFCC') >>> plt.show() Compare different DCT bases >>> m_slaney = librosa.feature.mfcc(y=y, sr=sr, dct_type=2) >>> m_htk = librosa.feature.mfcc(y=y, sr=sr, dct_type=3) >>> fig, ax = plt.subplots(nrows=2, sharex=True, sharey=True, layout='compressed') >>> img1 = librosa.display.specshow(m_slaney, x_axis='time', ax=ax[0]) >>> ax[0].set(title='RASTAMAT / Auditory toolbox (dct_type=2)') >>> fig.colorbar(img, ax=[ax[0]]) >>> img2 = librosa.display.specshow(m_htk, x_axis='time', ax=ax[1]) >>> ax[1].set(title='HTK-style (dct_type=3)') >>> fig.colorbar(img2, ax=[ax[1]]) >>> plt.show() """ if S is None: # multichannel behavior may be different due to relative noise floor differences between channels S = power_to_db(melspectrogram(y=y, sr=sr, norm = mel_norm, **kwargs)) import scipy.fft M: np.ndarray = scipy.fft.dct(S, axis=-2, type=dct_type, norm=norm)[ ..., :n_mfcc, : ] if lifter > 0: # shape lifter for broadcasting LI = np.sin(np.pi * np.arange(1, 1 + n_mfcc, dtype=M.dtype) / lifter) LI = util.expand_to(LI, ndim=S.ndim, axes=-2) M *= 1 + (lifter / 2) * LI return M elif lifter == 0: return M else: raise ParameterError(f"MFCC lifter={lifter} must be a non-negative number") def melspectrogram( *, y: np.ndarray | None = None, sr: float = 22050, S: np.ndarray | None = None, n_fft: int = 2048, hop_length: int = 512, win_length: int | None = None, window: _WindowSpec = "hann", center: bool = True, pad_mode: _PadModeSTFT = "constant", power: float = 2.0, **kwargs: Any, ) -> np.ndarray: """Compute a mel-scaled spectrogram. If a spectrogram input ``S`` is provided, then it is mapped directly onto the mel basis by ``mel_f.dot(S)``. If a time-series input ``y, sr`` is provided, then its magnitude spectrogram ``S`` is first computed, and then mapped onto the mel scale by ``mel_f.dot(S**power)``. By default, ``power=2`` operates on a power spectrum. Parameters ---------- y : np.ndarray [shape=(..., n)] or None audio time-series. Multi-channel is supported. sr : number > 0 [scalar] sampling rate of ``y`` S : np.ndarray [shape=(..., d, t)] spectrogram n_fft : int > 0 [scalar] length of the FFT frame hop_length : int > 0 [scalar] number of samples between successive frames. See `librosa.stft` win_length : int <= n_fft [scalar] Each frame of audio is windowed by `window()`. The window will be of length `win_length` and then padded with zeros to match ``n_fft``. If unspecified, defaults to ``win_length = n_fft``. window : str, tuple, number, function, or np.ndarray [shape=(n_fft,)] - a window specification (str, tuple, or number); see `scipy.signal.get_window` - a window function, such as `scipy.signal.windows.hann` - a vector or array of length ``n_fft`` .. see also:: `librosa.filters.get_window` center : bool - If `True`, the signal ``y`` is padded so that frame ``t`` is centered at ``y[t * hop_length]``. - If `False`, then frame ``t`` begins at ``y[t * hop_length]`` pad_mode : str If ``center=True``, the padding mode to use at the edges of the signal. By default, STFT uses zero padding. power : float > 0 [scalar] Exponent for the magnitude melspectrogram. e.g., 1 for energy, 2 for power, etc. **kwargs additional keyword arguments for Mel filter bank parameters n_mels : int > 0 [scalar] number of Mel bands to generate fmin : float >= 0 [scalar] lowest frequency (in Hz) fmax : float >= 0 [scalar] highest frequency (in Hz). If `None`, use ``fmax = sr / 2.0`` htk : bool [scalar] use HTK formula instead of Slaney norm : {None, 'slaney', or number} [scalar] If 'slaney', divide the triangular mel weights by the width of the mel band (area normalization). If numeric, use `librosa.util.normalize` to normalize each filter by to unit l_p norm. See `librosa.util.normalize` for a full description of supported norm values (including `+-np.inf`). Otherwise, leave all the triangles aiming for a peak value of 1.0 dtype : np.dtype The data type of the output basis. By default, uses 32-bit (single-precision) floating point. Returns ------- S : np.ndarray [shape=(..., n_mels, t)] Mel spectrogram See Also -------- librosa.filters.mel : Mel filter bank construction librosa.stft : Short-time Fourier Transform Examples -------- >>> y, sr = librosa.loadx('trumpet') >>> librosa.feature.melspectrogram(y=y, sr=sr) array([[3.837e-06, 1.451e-06, ..., 8.352e-14, 1.296e-11], [2.213e-05, 7.866e-06, ..., 8.532e-14, 1.329e-11], ..., [1.115e-05, 5.192e-06, ..., 3.675e-08, 2.470e-08], [6.473e-07, 4.402e-07, ..., 1.794e-08, 2.908e-08]], dtype=float32) Using a pre-computed power spectrogram would give the same result: >>> D = np.abs(librosa.stft(y))**2 >>> S = librosa.feature.melspectrogram(S=D, sr=sr) Display of mel-frequency spectrogram coefficients, with custom arguments for mel filterbank construction (default is fmax=sr/2): >>> # Passing through arguments to the Mel filters >>> S = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=128, ... fmax=8000) >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots() >>> S_dB = librosa.power_to_db(S, ref=np.max) >>> img = librosa.display.specshow(S_dB, x_axis='time', ... y_axis='mel', sr=sr, ... fmax=8000, ax=ax) >>> librosa.display.colorbar_db(img) >>> ax.set(title='Mel-frequency spectrogram') """ S, n_fft = _spectrogram( y=y, S=S, n_fft=n_fft, hop_length=hop_length, power=power, win_length=win_length, window=window, center=center, pad_mode=pad_mode, ) # Build a Mel filter mel_basis = filters.mel(sr=sr, n_fft=n_fft, **kwargs) melspec: np.ndarray = np.einsum("...ft,mf->...mt", S, mel_basis, optimize=True) return melspec ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2125447 librosa-1.0.0/librosa/feature/utils.py0000644000175100017510000002346215236640653017432 0ustar00runnerrunner#!/usr/bin/env python # -*- coding: utf-8 -*- """Feature manipulation utilities""" from __future__ import annotations from typing import TYPE_CHECKING import numpy as np from numba import jit from .._cache import cache from ..util.exceptions import ParameterError if TYPE_CHECKING: from typing import Any, Literal __all__ = ["delta", "stack_memory"] @cache(level=40) def delta( data: np.ndarray, *, width: int = 9, order: int = 1, axis: int = -1, mode: Literal["interp", "nearest", "mirror", "constant", "wrap"] = "interp", **kwargs: Any, ) -> np.ndarray: r"""Compute delta features: local estimate of the derivative of the input data along the selected axis. Delta features are computed Savitsky-Golay filtering. Parameters ---------- data : np.ndarray the input data matrix (eg, spectrogram) width : int, positive, odd [scalar] Number of frames over which to compute the delta features. Cannot exceed the length of ``data`` along the specified axis. If ``mode='interp'``, then ``width`` must be at least ``data.shape[axis]``. order : int > 0 [scalar] the order of the difference operator. 1 for first derivative, 2 for second, etc. axis : int [scalar] the axis along which to compute deltas. Default is -1 (columns). mode : str, {'interp', 'nearest', 'mirror', 'constant', 'wrap'} Padding mode for estimating differences at the boundaries. **kwargs : additional keyword arguments See `scipy.signal.savgol_filter` Returns ------- delta_data : np.ndarray [shape=(..., t)] delta matrix of ``data`` at specified order Notes ----- This function caches at level 40. See Also -------- scipy.signal.savgol_filter Examples -------- Compute MFCC deltas, delta-deltas >>> y, sr = librosa.loadx('libri1', duration=5) >>> mfcc = librosa.feature.mfcc(y=y, sr=sr) >>> mfcc_delta = librosa.feature.delta(mfcc) >>> mfcc_delta array([[-5.713e+02, -5.697e+02, ..., -1.522e+02, -1.224e+02], [ 1.104e+01, 1.330e+01, ..., 2.089e+02, 1.698e+02], ..., [ 2.829e+00, 1.933e+00, ..., -3.149e+00, 2.294e-01], [ 2.890e+00, 2.187e+00, ..., 6.959e+00, -1.039e+00]], dtype=float32) >>> mfcc_delta2 = librosa.feature.delta(mfcc, order=2) >>> mfcc_delta2 array([[-1.195, -1.195, ..., -4.328, -4.328], [-1.566, -1.566, ..., -9.949, -9.949], ..., [ 0.707, 0.707, ..., 2.287, 2.287], [ 0.655, 0.655, ..., -1.719, -1.719]], dtype=float32) >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=3, sharex=True, sharey=True) >>> img1 = librosa.display.specshow(mfcc, ax=ax[0], x_axis='time') >>> ax[0].set(title='MFCC') >>> ax[0].label_outer() >>> img2 = librosa.display.specshow(mfcc_delta, ax=ax[1], x_axis='time') >>> ax[1].set(title=r'MFCC-$\Delta$') >>> ax[1].label_outer() >>> img3 = librosa.display.specshow(mfcc_delta2, ax=ax[2], x_axis='time') >>> ax[2].set(title=r'MFCC-$\Delta^2$') >>> fig.colorbar(img1, ax=[ax[0]]) >>> fig.colorbar(img2, ax=[ax[1]]) >>> fig.colorbar(img3, ax=[ax[2]]) """ data = np.atleast_1d(data) if mode == "interp" and width > data.shape[axis]: raise ParameterError( f"when mode='interp', width={width} " f"cannot exceed data.shape[axis]={data.shape[axis]}" ) if width < 3 or np.mod(width, 2) != 1: raise ParameterError("width must be an odd integer >= 3") if order <= 0 or not isinstance(order, (int, np.integer)): raise ParameterError("order must be a positive integer") kwargs.pop("deriv", None) kwargs.setdefault("polyorder", order) import scipy.signal result: np.ndarray = scipy.signal.savgol_filter( data, width, deriv=order, axis=axis, mode=mode, **kwargs ) return result @cache(level=40) def stack_memory( data: np.ndarray, *, n_steps: int = 2, delay: int = 1, **kwargs: Any ) -> np.ndarray: """Short-term history embedding: concatenate an array with delayed copies of itself. Each column ``data[:, i]`` is mapped to:: data[..., i] -> [data[..., i], data[..., i - delay], ... data[..., i - (n_steps-1)*delay]] For columns ``i < (n_steps - 1) * delay``, the data will be padded. By default, the data is padded with zeros, but this behavior can be overridden by supplying additional keyword arguments which are passed to `np.pad()`. Parameters ---------- data : np.ndarray [shape=(..., d, t)] Input data matrix. If ``data`` is a vector (``data.ndim == 1``), it will be interpreted as a row matrix and reshaped to ``(1, t)``. n_steps : int > 0 [scalar] embedding dimension, the number of steps back in time to stack delay : int != 0 [scalar] the number of columns to step. Positive values embed from the past (previous columns). Negative values embed from the future (subsequent columns). **kwargs : additional keyword arguments Additional arguments to pass to `numpy.pad` Returns ------- data_history : np.ndarray [shape=(..., m * d, t)] data augmented with lagged copies of itself, where ``m == n_steps - 1``. Notes ----- This function caches at level 40. Examples -------- Keep two steps (current and previous) >>> data = np.arange(-3, 3) >>> librosa.feature.stack_memory(data) array([[-3, -2, -1, 0, 1, 2], [ 0, -3, -2, -1, 0, 1]]) Or three steps >>> librosa.feature.stack_memory(data, n_steps=3) array([[-3, -2, -1, 0, 1, 2], [ 0, -3, -2, -1, 0, 1], [ 0, 0, -3, -2, -1, 0]]) Use reflection padding instead of zero-padding >>> librosa.feature.stack_memory(data, n_steps=3, mode='reflect') array([[-3, -2, -1, 0, 1, 2], [-2, -3, -2, -1, 0, 1], [-1, -2, -3, -2, -1, 0]]) Or pad with edge-values, and delay by 2 >>> librosa.feature.stack_memory(data, n_steps=3, delay=2, mode='edge') array([[-3, -2, -1, 0, 1, 2], [-3, -3, -3, -2, -1, 0], [-3, -3, -3, -3, -3, -2]]) Stack time-lagged beat-synchronous chroma edge padding >>> y, sr = librosa.loadx('sweetwaltz', duration=10) >>> chroma = librosa.feature.chroma_cqt(y=y, sr=sr) >>> tempo, beats = librosa.beat.beat_track(y=y, sr=sr, hop_length=512) >>> beats = librosa.util.fix_frames(beats, x_min=0) >>> chroma_sync = librosa.util.sync(chroma, beats) >>> chroma_lag = librosa.feature.stack_memory(chroma_sync, n_steps=3, ... mode='edge') Plot the result >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots() >>> beat_times = librosa.frames_to_time(beats, sr=sr, hop_length=512) >>> librosa.display.specshow(chroma_lag, y_axis='chroma', x_axis='time', ... x_coords=beat_times, ax=ax) >>> ax.text(1.0, 1/6, "Lag=0", transform=ax.transAxes, rotation=-90, ha="left", va="center") >>> ax.text(1.0, 3/6, "Lag=1", transform=ax.transAxes, rotation=-90, ha="left", va="center") >>> ax.text(1.0, 5/6, "Lag=2", transform=ax.transAxes, rotation=-90, ha="left", va="center") >>> ax.set(title='Time-lagged chroma', ylabel="") """ if n_steps < 1: raise ParameterError("n_steps must be a positive integer") if delay == 0: raise ParameterError("delay must be a non-zero integer") data = np.atleast_2d(data) t = data.shape[-1] if t < 1: raise ParameterError( "Cannot stack memory when input data has " f"no columns. Given data.shape={data.shape}" ) kwargs.setdefault("mode", "constant") if kwargs["mode"] == "constant": kwargs.setdefault("constant_values", [0]) padding = [(0, 0) for _ in range(data.ndim)] # Pad the end with zeros, which will roll to the front below if delay > 0: padding[-1] = (int((n_steps - 1) * delay), 0) else: padding[-1] = (0, int((n_steps - 1) * -delay)) data = np.pad(data, padding, **kwargs) # Construct the shape of the target array shape = list(data.shape) shape[-2] = shape[-2] * n_steps shape[-1] = t shape = tuple(shape) # Construct the output array to match layout and dtype of input history = np.empty_like(data, shape=shape) # Populate the output array __stack(history, data, n_steps, delay) return history @jit(nopython=True, cache=True) def __stack(history, data, n_steps, delay): """Memory-stacking helper function. Parameters ---------- history : output array (2-dimensional) data : pre-padded input array (2-dimensional) n_steps : int > 0, the number of steps to stack delay : int != 0, the amount of delay between steps Returns ------- None Output is stored directly in the history array """ # Dimension of each copy of the data d = data.shape[-2] # Total number of time-steps to output t = history.shape[-1] if delay > 0: for step in range(n_steps): q = n_steps - 1 - step # nth block is original shifted left by n*delay steps history[..., step * d : (step + 1) * d, :] = data[ ..., q * delay : q * delay + t ] else: # Handle the last block separately to avoid -t:0 empty slices history[..., -d:, :] = data[..., -t:] for step in range(n_steps - 1): # nth block is original shifted right by n*delay steps q = n_steps - 1 - step history[..., step * d : (step + 1) * d, :] = data[ ..., -t + q * delay : q * delay ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2125447 librosa-1.0.0/librosa/filters.py0000644000175100017510000012416215236640653016306 0ustar00runnerrunner#!/usr/bin/env python # -*- coding: utf-8 -*- """ librosa.filters provides functionality for filter bank construction, window function processing, and related utilities. """ from __future__ import annotations import warnings from typing import TYPE_CHECKING import numpy as np from numba import jit from . import util from ._cache import cache from .core.convert import fft_frequencies, hz_to_midi, hz_to_octs, mel_frequencies, midi_to_hz, note_to_hz from .util.exceptions import ParameterError if TYPE_CHECKING: from typing import Any, Literal from numpy.typing import ArrayLike, DTypeLike, NDArray from ._typing import _FloatLike_co, _WindowSpec __all__ = [ "mel", "chroma", "cq_to_chroma", "window_bandwidth", "get_window", "mr_frequencies", "semitone_filterbank", "window_sumsquare", "diagonal_filter", "wavelet", "wavelet_lengths", ] # Dictionary of window function bandwidths WINDOW_BANDWIDTHS: dict[Any, float] = { "bart": 1.3334961334912805, "barthann": 1.4560255965133932, "bartlett": 1.3334961334912805, "bkh": 2.0045975283585014, "black": 1.7269681554262326, "blackharr": 2.0045975283585014, "blackman": 1.7269681554262326, "blackmanharris": 2.0045975283585014, "blk": 1.7269681554262326, "bman": 1.7859588613860062, "bmn": 1.7859588613860062, "bohman": 1.7859588613860062, "box": 1.0, "boxcar": 1.0, "brt": 1.3334961334912805, "brthan": 1.4560255965133932, "bth": 1.4560255965133932, "cosine": 1.2337005350199792, "flat": 2.7762255046484143, "flattop": 2.7762255046484143, "flt": 2.7762255046484143, "halfcosine": 1.2337005350199792, "ham": 1.3629455320350348, "hamm": 1.3629455320350348, "hamming": 1.3629455320350348, "han": 1.50018310546875, "hann": 1.50018310546875, "nut": 1.9763500280946082, "nutl": 1.9763500280946082, "nuttall": 1.9763500280946082, "ones": 1.0, "par": 1.9174603174603191, "parz": 1.9174603174603191, "parzen": 1.9174603174603191, "rect": 1.0, "rectangular": 1.0, "tri": 1.3331706523555851, "triang": 1.3331706523555851, "triangle": 1.3331706523555851, } @cache(level=10) def mel( *, sr: float, n_fft: int, n_mels: int = 128, fmin: float = 0.0, fmax: float | None = None, htk: bool = False, norm: Literal["slaney"] | float | None = "slaney", dtype: DTypeLike = np.float32, ) -> np.ndarray: """Create a Mel filter-bank. This produces a linear transformation matrix to project FFT bins onto Mel-frequency bins. Parameters ---------- sr : number > 0 [scalar] sampling rate of the incoming signal n_fft : int > 0 [scalar] length of the FFT frame; the filter bank has ``1 + n_fft // 2`` frequency bins n_mels : int > 0 [scalar] number of Mel bands to generate fmin : float >= 0 [scalar] lowest frequency (in Hz) fmax : float >= 0 [scalar] highest frequency (in Hz). If `None`, use ``fmax = sr / 2.0`` htk : bool [scalar] use HTK formula instead of Slaney norm : {None, 'slaney', or number} [scalar] If 'slaney', divide the triangular mel weights by the width of the mel band (area normalization). If numeric, use `librosa.util.normalize` to normalize each filter by to unit l_p norm. See `librosa.util.normalize` for a full description of supported norm values (including `+-np.inf`). Otherwise, leave all the triangles aiming for a peak value of 1.0 dtype : np.dtype The data type of the output basis. By default, uses 32-bit (single-precision) floating point. Returns ------- M : np.ndarray [shape=(n_mels, 1 + n_fft/2)] Mel transform matrix See Also -------- librosa.util.normalize Notes ----- This function caches at level 10. Examples -------- >>> melfb = librosa.filters.mel(sr=22050, n_fft=2048) >>> melfb array([[ 0. , 0.016, ..., 0. , 0. ], [ 0. , 0. , ..., 0. , 0. ], ..., [ 0. , 0. , ..., 0. , 0. ], [ 0. , 0. , ..., 0. , 0. ]]) Clip the maximum frequency to 8KHz >>> librosa.filters.mel(sr=22050, n_fft=2048, fmax=8000) array([[ 0. , 0.02, ..., 0. , 0. ], [ 0. , 0. , ..., 0. , 0. ], ..., [ 0. , 0. , ..., 0. , 0. ], [ 0. , 0. , ..., 0. , 0. ]]) >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots() >>> img = librosa.display.specshow(melfb, x_axis='linear', ax=ax) >>> ax.set(ylabel='Mel filter', title='Mel filter bank') >>> fig.colorbar(img, ax=ax) """ if fmax is None: fmax = float(sr) / 2 # Initialize the weights n_mels = int(n_mels) weights = np.zeros((n_mels, int(1 + n_fft // 2)), dtype=dtype) # Center freqs of each FFT bin fftfreqs = fft_frequencies(sr=sr, n_fft=n_fft) # 'Center freqs' of mel bands - uniformly spaced between limits mel_f = mel_frequencies(n_mels + 2, fmin=fmin, fmax=fmax, htk=htk) fdiff = np.diff(mel_f) ramps = np.subtract.outer(mel_f, fftfreqs) for i in range(n_mels): # lower and upper slopes for all bins lower = -ramps[i] / fdiff[i] upper = ramps[i + 2] / fdiff[i + 1] # .. then intersect them with each other and zero weights[i] = np.maximum(0, np.minimum(lower, upper)) if isinstance(norm, str): if norm == "slaney": # Slaney-style mel is scaled to be approx constant energy per channel enorm = 2.0 / (mel_f[2 : n_mels + 2] - mel_f[:n_mels]) weights *= enorm[:, np.newaxis] else: raise ParameterError(f"Unsupported norm={norm}") else: weights = util.normalize(weights, norm=norm, axis=-1) # Only check weights if f_mel[0] is positive if not np.all((mel_f[:-2] == 0) | (weights.max(axis=1) > 0)): # This means we have an empty channel somewhere warnings.warn( "Empty filters detected in mel frequency basis. " "Some channels will produce empty responses. " "Try increasing your sampling rate (and fmax) or " "reducing n_mels.", stacklevel=2, ) return weights @cache(level=10) def chroma( *, sr: float, n_fft: int, n_chroma: int = 12, tuning: float = 0.0, ctroct: float = 5.0, octwidth: float | None = 2, norm: float | None = 2, base_c: bool = True, dtype: DTypeLike = np.float32, ) -> np.ndarray: """Create a chroma filter bank. This creates a linear transformation matrix to project FFT bins onto chroma bins (i.e. pitch classes). Parameters ---------- sr : number > 0 [scalar] audio sampling rate n_fft : int > 0 [scalar] length of the FFT frame; the filter bank has ``1 + n_fft // 2`` frequency bins n_chroma : int > 0 [scalar] number of chroma bins tuning : float Tuning deviation from A440 in fractions of a chroma bin. ctroct : float > 0 [scalar] Center of the octave dominance window (in octaves, A0 = 27.5 Hz). octwidth : float > 0 or None [scalar] ``ctroct`` and ``octwidth`` specify a dominance window: a Gaussian weighting centered on ``ctroct`` (in octs, A0 = 27.5Hz) and with a gaussian half-width of ``octwidth``. Set ``octwidth`` to `None` to use a flat weighting. norm : float > 0 or np.inf Normalization factor for each filter base_c : bool If True, the filter bank will start at 'C'. If False, the filter bank will start at 'A'. dtype : np.dtype The data type of the output basis. By default, uses 32-bit (single-precision) floating point. Returns ------- wts : ndarray [shape=(n_chroma, 1 + n_fft / 2)] Chroma filter matrix See Also -------- librosa.util.normalize librosa.feature.chroma_stft Notes ----- This function caches at level 10. Examples -------- Build a simple chroma filter bank >>> chromafb = librosa.filters.chroma(sr=22050, n_fft=4096) array([[ 1.689e-05, 3.024e-04, ..., 4.639e-17, 5.327e-17], [ 1.716e-05, 2.652e-04, ..., 2.674e-25, 3.176e-25], ..., [ 1.578e-05, 3.619e-04, ..., 8.577e-06, 9.205e-06], [ 1.643e-05, 3.355e-04, ..., 1.474e-10, 1.636e-10]]) Use quarter-tones instead of semitones >>> librosa.filters.chroma(sr=22050, n_fft=4096, n_chroma=24) array([[ 1.194e-05, 2.138e-04, ..., 6.297e-64, 1.115e-63], [ 1.206e-05, 2.009e-04, ..., 1.546e-79, 2.929e-79], ..., [ 1.162e-05, 2.372e-04, ..., 6.417e-38, 9.923e-38], [ 1.180e-05, 2.260e-04, ..., 4.697e-50, 7.772e-50]]) Equally weight all octaves >>> librosa.filters.chroma(sr=22050, n_fft=4096, octwidth=None) array([[ 3.036e-01, 2.604e-01, ..., 2.445e-16, 2.809e-16], [ 3.084e-01, 2.283e-01, ..., 1.409e-24, 1.675e-24], ..., [ 2.836e-01, 3.116e-01, ..., 4.520e-05, 4.854e-05], [ 2.953e-01, 2.888e-01, ..., 7.768e-10, 8.629e-10]]) >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots() >>> img = librosa.display.specshow(chromafb, x_axis='linear', ax=ax) >>> ax.set(ylabel='Chroma filter', title='Chroma filter bank') >>> fig.colorbar(img, ax=ax) """ wts = np.zeros((n_chroma, n_fft)) # Get the FFT bins, not counting the DC component frequencies = np.linspace(0, sr, n_fft, endpoint=False)[1:] frqbins = n_chroma * hz_to_octs( frequencies, tuning=tuning, bins_per_octave=n_chroma ) # make up a value for the 0 Hz bin = 1.5 octaves below bin 1 # (so chroma is 50% rotated from bin 1, and bin width is broad) frqbins = np.concatenate(([frqbins[0] - 1.5 * n_chroma], frqbins)) binwidthbins = np.concatenate((np.maximum(frqbins[1:] - frqbins[:-1], 1.0), [1])) D = np.subtract.outer(frqbins, np.arange(0, n_chroma, dtype="d")).T n_chroma2 = np.round(float(n_chroma) / 2) # Project into range -n_chroma/2 .. n_chroma/2 # add on fixed offset of 10*n_chroma to ensure all values passed to # rem are positive D = np.remainder(D + n_chroma2 + 10 * n_chroma, n_chroma) - n_chroma2 # Gaussian bumps - 2*D to make them narrower wts = np.exp(-0.5 * (2 * D / np.tile(binwidthbins, (n_chroma, 1))) ** 2) # normalize each column wts = util.normalize(wts, norm=norm, axis=0) # Maybe apply scaling for fft bins if octwidth is not None: wts *= np.exp(-0.5 * (((frqbins / n_chroma - ctroct) / octwidth) ** 2))[np.newaxis, :] if base_c: wts = np.roll(wts, -3 * (n_chroma // 12), axis=0) # remove aliasing columns, copy to ensure row-contiguity return np.ascontiguousarray(wts[:, : int(1 + n_fft / 2)], dtype=dtype) def __float_window(window_spec): """Decorate a window function to support fractional input lengths. This function guarantees that for fractional ``x``, the following hold: 1. ``__float_window(window_function)(x)`` has length ``np.ceil(x)`` 2. all values from ``np.floor(x)`` are set to 0. For integer-valued ``x``, there should be no change in behavior. """ def _wrap(n, *args, **kwargs): """Wrap the window""" n_min, n_max = int(np.floor(n)), int(np.ceil(n)) window = get_window(window_spec, n_min) if len(window) < n_max: window = np.pad(window, [(0, n_max - len(window))], mode="constant") window[n_min:] = 0.0 return window return _wrap @cache(level=10) def wavelet_lengths( *, freqs: ArrayLike, sr: float = 22050, window: _WindowSpec = "hann", filter_scale: float = 1, gamma: float | None = 0, alpha: float | np.ndarray | None = None, ) -> tuple[NDArray[np.float64], float]: """Return length of each filter in a wavelet basis. Parameters ---------- freqs : np.ndarray (positive) Center frequencies of the filters (in Hz). Must be in ascending order. sr : number > 0 [scalar] Audio sampling rate window : str or callable Window function to use on filters filter_scale : float > 0 [scalar] Resolution of filter windows. Larger values use longer windows. gamma : number >= 0 [scalar, optional] Bandwidth offset for determining filter lengths, as used in Variable-Q transforms. Bandwidth for the k'th filter is determined by:: B[k] = alpha[k] * freqs[k] + gamma ``alpha[k]`` is twice the relative difference between ``freqs[k+1]`` and ``freqs[k-1]``:: alpha[k] = (freqs[k+1]-freqs[k-1]) / (freqs[k+1]+freqs[k-1]) If ``freqs`` follows a geometric progression (as in CQT and VQT), the vector ``alpha`` is constant and such that:: (1 + alpha) * freqs[k-1] = (1 - alpha) * freqs[k+1] Furthermore, if ``gamma=0`` (default), ``alpha`` is such that even-``k`` and odd-``k`` filters are interleaved:: freqs[k-1] + B[k-1] = freqs[k+1] - B[k+1] If ``gamma=None`` is specified, then ``gamma`` is computed such that each filter has bandwidth proportional to the equivalent rectangular bandwidth (ERB) at frequency ``freqs[k]``:: gamma[k] = 24.7 * alpha[k] / 0.108 as derived by [#]_. .. [#] Glasberg, Brian R., and Brian CJ Moore. "Derivation of auditory filter shapes from notched-noise data." Hearing research 47.1-2 (1990): 103-138. alpha : number > 0 [optional] Optional pre-computed relative bandwidth parameter. Note that this must be provided if ``len(freqs)==1`` because bandwidth cannot be inferred from a single frequency. Otherwise, if left unspecified, it will be automatically derived by the rules specified above. Returns ------- lengths : np.ndarray The length of each filter. f_cutoff : float The lowest frequency at which all filters' main lobes have decayed by at least 3dB. This second output serves in cqt and vqt to ensure that all wavelet bands remain below the Nyquist frequency. Notes ----- This function caches at level 10. Raises ------ ParameterError - If ``filter_scale`` is not strictly positive - If ``gamma`` is a negative number - If any frequencies are <= 0 - If the frequency array is not sorted in ascending order """ freqs = np.asarray(freqs) if filter_scale <= 0: raise ParameterError(f"filter_scale={filter_scale} must be positive") if gamma is not None and gamma < 0: raise ParameterError(f"gamma={gamma} must be non-negative") if np.any(freqs <= 0): raise ParameterError("frequencies must be strictly positive") if len(freqs) > 1 and np.any(freqs[:-1] > freqs[1:]): raise ParameterError( f"Frequency array={freqs} must be in strictly ascending order" ) if alpha is None: alpha = _relative_bandwidth(freqs=freqs) else: alpha = np.asarray(alpha) gamma_: _FloatLike_co | np.ndarray if gamma is None: gamma_ = alpha * 24.7 / 0.108 else: gamma_ = gamma # Q should be capitalized here, so we suppress the name warning # pylint: disable=invalid-name Q = float(filter_scale) / alpha # How far up does our highest frequency reach? f_cutoff = max(freqs * (1 + 0.5 * window_bandwidth(window) / Q) + 0.5 * gamma_) # Convert frequencies to filter lengths lengths = Q * sr / (freqs + gamma_ / alpha) return lengths, f_cutoff def _relative_bandwidth(*, freqs: np.ndarray) -> np.ndarray: """Compute the relative bandwidth for each of a set of specified frequencies. This function is used as a helper in wavelet basis construction. Parameters ---------- freqs : np.ndarray The array of frequencies Returns ------- alpha : np.ndarray Relative bandwidth """ if len(freqs) <= 1: raise ParameterError(f"2 or more frequencies are required to compute bandwidths. Given freqs={freqs}") # Approximate the local octave resolution around each frequency bpo = np.empty_like(freqs) logf = np.log2(freqs) # Reflect at the lowest and highest frequencies bpo[0] = 1 / (logf[1] - logf[0]) bpo[-1] = 1 / (logf[-1] - logf[-2]) # For everything else, do a centered difference bpo[1:-1] = 2 / (logf[2:] - logf[:-2]) # Compute relative bandwidths alpha = (2.0 ** (2 / bpo) - 1) / (2.0 ** (2 / bpo) + 1) return alpha @cache(level=10) def wavelet( *, freqs: np.ndarray, sr: float = 22050, window: _WindowSpec = "hann", filter_scale: float = 1, pad_fft: bool = True, norm: float | None = 1, dtype: DTypeLike = np.complex64, gamma: float = 0, alpha: float | None = None, **kwargs: Any, ) -> tuple[np.ndarray, np.ndarray]: """Construct a wavelet basis using windowed complex sinusoids. This function constructs a wavelet filterbank at a specified set of center frequencies. Parameters ---------- freqs : np.ndarray (positive) Center frequencies of the filters (in Hz). Must be in ascending order. sr : number > 0 [scalar] Audio sampling rate window : str, tuple, number, or function Windowing function to apply to filters. filter_scale : float > 0 [scalar] Scale of filter windows. Small values (<1) use shorter windows for higher temporal resolution. pad_fft : bool Center-pad all filters up to the nearest integral power of 2. By default, padding is done with zeros, but this can be overridden by setting the ``mode=`` field in *kwargs*. norm : {inf, -inf, 0, float > 0} Type of norm to use for basis function normalization. See librosa.util.normalize dtype : np.dtype The data type of the output basis. By default, uses 64-bit (single precision) complex floating point. gamma : number >= 0 Bandwidth offset for variable-Q transforms. alpha : number > 0 [optional] Optional pre-computed relative bandwidth parameter. Note that this must be provided if ``len(freqs)==1`` because bandwidth cannot be inferred from a single frequency. Otherwise, if left unspecified, it will be automatically derived by the rules specified above. **kwargs : additional keyword arguments Arguments to `np.pad()` when ``pad==True``. Returns ------- filters : np.ndarray, ``len(filters) == n_bins`` each ``filters[i]`` is a (complex) time-domain filter lengths : np.ndarray, ``len(lengths) == n_bins`` The (fractional) length of each filter in samples Notes ----- This function caches at level 10. See Also -------- wavelet_lengths librosa.cqt librosa.vqt librosa.util.normalize Examples -------- Create a constant-Q basis >>> freqs = librosa.cqt_frequencies(n_bins=84, fmin=librosa.note_to_hz('C1')) >>> basis, lengths = librosa.filters.wavelet(freqs=freqs, sr=22050) Plot one octave of filters in time and frequency >>> import matplotlib.pyplot as plt >>> basis, lengths = librosa.filters.wavelet(freqs=freqs, sr=22050) >>> fig, ax = plt.subplots(nrows=2, figsize=(10, 6)) >>> notes = librosa.midi_to_note(np.arange(24, 24 + len(basis))) >>> for i, (f, n) in enumerate(zip(basis, notes[:12])): ... f_scale = librosa.util.normalize(f) / 2 ... ax[0].plot(i + f_scale.real) ... ax[0].plot(i + f_scale.imag, linestyle=':') >>> ax[0].set(yticks=np.arange(len(notes[:12])), yticklabels=notes[:12], ... ylabel='CQ filters', ... title='CQ filters (one octave, time domain)', ... xlabel='Time (samples at 22050 Hz)') >>> ax[0].legend(['Real', 'Imaginary']) >>> F = np.abs(np.fft.fftn(basis, axes=[-1])) >>> # Keep only the positive frequencies >>> F = F[:, :(1 + F.shape[1] // 2)] >>> librosa.display.specshow(F, x_axis='linear', y_axis='cqt_note', ax=ax[1]) >>> ax[1].set(ylabel='CQ filters', title='CQ filter magnitudes (frequency domain)') """ # Pass-through parameters to get the filter lengths lengths, _ = wavelet_lengths( freqs=freqs, sr=sr, window=window, filter_scale=filter_scale, gamma=gamma, alpha=alpha, ) # Build the filters filters = [] for ilen, freq in zip(lengths, freqs, strict=True): # Build the filter: note, length will be ceil(ilen) sig = util.phasor( np.arange(-ilen // 2, ilen // 2, dtype=float) * 2 * np.pi * freq / sr ) # Apply the windowing function sig *= __float_window(window)(len(sig)) # Normalize sig = util.normalize(sig, norm=norm) filters.append(sig) # Pad and stack max_len = max(lengths) if pad_fft: max_len = int(2.0 ** (np.ceil(np.log2(max_len)))) else: max_len = int(np.ceil(max_len)) filters = np.asarray( [util.pad_center(filt, size=max_len, **kwargs) for filt in filters], dtype=dtype ) return filters, lengths @cache(level=10) def cq_to_chroma( n_input: int, *, bins_per_octave: int = 12, n_chroma: int = 12, fmin: _FloatLike_co | None = None, window: np.ndarray | None = None, base_c: bool = True, dtype: DTypeLike = np.float32, ) -> np.ndarray: """Construct a linear transformation matrix to map Constant-Q bins onto chroma bins. Parameters ---------- n_input : int > 0 [scalar] Number of input components (CQT bins) bins_per_octave : int > 0 [scalar] How many bins per octave in the CQT n_chroma : int > 0 [scalar] Number of output bins (per octave) in the chroma fmin : None or float > 0 Center frequency of the first constant-Q channel. Default: 'C1' ~= 32.7 Hz window : None or np.ndarray If provided, the cq_to_chroma filter bank will be convolved with ``window``. base_c : bool If True, the first chroma bin will start at 'C' If False, the first chroma bin will start at 'A' dtype : np.dtype The data type of the output basis. By default, uses 32-bit (single-precision) floating point. Returns ------- cq_to_chroma : np.ndarray [shape=(n_chroma, n_input)] Transformation matrix: ``Chroma = np.dot(cq_to_chroma, CQT)`` Raises ------ ParameterError If ``n_input`` is not an integer multiple of ``n_chroma`` Notes ----- This function caches at level 10. Examples -------- Get a CQT, and wrap bins to chroma >>> y, sr = librosa.loadx('trumpet') >>> CQT = np.abs(librosa.cqt(y, sr=sr)) >>> chroma_map = librosa.filters.cq_to_chroma(CQT.shape[0]) >>> chromagram = chroma_map.dot(CQT) >>> # Max-normalize each time step >>> chromagram = librosa.util.normalize(chromagram, axis=0) >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=3, sharex=True) >>> imgcq = librosa.display.specshow(CQT, vscale='dBFS', ... y_axis='cqt_note', x_axis='time', ... ax=ax[0]) >>> ax[0].set(title='CQT Magnitude') >>> ax[0].label_outer() >>> librosa.display.specshow(chromagram, y_axis='chroma', x_axis='time', ... ax=ax[1]) >>> ax[1].set(title='Chroma (wrapped CQT)') >>> ax[1].label_outer() >>> chroma = librosa.feature.chroma_stft(y=y, sr=sr) >>> imgchroma = librosa.display.specshow(chroma, y_axis='chroma', x_axis='time', ax=ax[2]) >>> ax[2].set(title='librosa.feature.chroma_stft') """ # How many fractional bins are we merging? n_merge = float(bins_per_octave) / n_chroma fmin_: _FloatLike_co if fmin is None: fmin_ = note_to_hz("C1") else: fmin_ = fmin if np.mod(n_merge, 1) != 0: raise ParameterError( "Incompatible CQ merge: " "input bins must be an " "integer multiple of output bins." ) # Tile the identity to merge fractional bins cq_to_ch = np.repeat(np.eye(n_chroma), int(n_merge), axis=1) # Roll it left to center on the target bin cq_to_ch = np.roll(cq_to_ch, -int(n_merge // 2), axis=1) # How many octaves are we repeating? n_octaves = np.ceil(float(n_input) / bins_per_octave) # Repeat and trim cq_to_ch = np.tile(cq_to_ch, int(n_octaves))[:, :n_input] # What's the note number of the first bin in the CQT? # midi uses 12 bins per octave here midi_0 = np.mod(hz_to_midi(fmin_), 12) if base_c: # rotate to C roll = midi_0 else: # rotate to A roll = midi_0 - 9 # Adjust the roll in terms of how many chroma we want out # We need to be careful with rounding here roll = int(np.round(roll * (n_chroma / 12.0))) # Apply the roll cq_to_ch = np.roll(cq_to_ch, roll, axis=0).astype(dtype) if window is not None: import scipy.signal cq_to_ch = scipy.signal.convolve(cq_to_ch, np.atleast_2d(window), mode="same") return cq_to_ch @cache(level=10) def window_bandwidth(window: _WindowSpec, n: int = 1000) -> float: """Get the equivalent noise bandwidth (ENBW) of a window function. The ENBW of a window is defined by [#]_ (equation 11) as the normalized ratio of the sum of squares to the square of sums:: enbw = n * sum(window**2) / sum(window)**2 .. [#] Harris, F. J. "On the use of windows for harmonic analysis with the discrete Fourier transform." Proceedings of the IEEE, 66(1), 51-83. 1978. Parameters ---------- window : callable or str A window function, or the name of a window function, e.g.: `scipy.signal.hann` or `'boxcar'` n : int > 0 The number of coefficients to use in estimating the window bandwidth Returns ------- bandwidth : float The equivalent noise bandwidth (in FFT bins) of the given window function Notes ----- This function caches at level 10. See Also -------- get_window """ if hasattr(window, "__name__"): key = window.__name__ else: key = window if key not in WINDOW_BANDWIDTHS: win = get_window(window, n) WINDOW_BANDWIDTHS[key] = ( n * np.sum(win**2) / (np.sum(win) ** 2 + util.tiny(win)) ) return WINDOW_BANDWIDTHS[key] @cache(level=10) def get_window(window: _WindowSpec, Nx: int, *, fftbins: bool = True) -> np.ndarray: """Compute a window function. This is a wrapper for `scipy.signal.get_window` that additionally supports callable or pre-computed windows. Parameters ---------- window : str, tuple, number, callable, or list-like The window specification: - If string, it's the name of the window function (e.g., `'hann'`) - If tuple, it's the name of the window function and any parameters (e.g., `('kaiser', 4.0)`) - If numeric, it is treated as the beta parameter of the `'kaiser'` window, as in `scipy.signal.get_window`. - If callable, it's a function that accepts one integer argument (the window length) - If list-like, it's a pre-computed window of the correct length `Nx` Nx : int > 0 The length of the window fftbins : bool, optional If True (default), create a periodic window for use with FFT If False, create a symmetric window for filter design applications. Returns ------- get_window : np.ndarray A window of length `Nx` and type `window` See Also -------- scipy.signal.get_window Notes ----- This function caches at level 10. Raises ------ ParameterError If `window` is supplied as a vector of length != `n_fft`, or is otherwise mis-specified. """ if callable(window): return window(Nx) elif isinstance(window, (str, tuple)) or np.isscalar(window): # TODO: if we add custom window functions in librosa, call them here import scipy.signal win: np.ndarray = scipy.signal.get_window(window, Nx, fftbins=fftbins) # type: ignore return win elif isinstance(window, (np.ndarray, list)): if len(window) == Nx: return np.asarray(window) raise ParameterError(f"Window size mismatch: {len(window):d} != {Nx:d}") else: raise ParameterError(f"Invalid window specification: {window!r}") @cache(level=10) def _multirate_fb( center_freqs: np.ndarray | None = None, sample_rates: np.ndarray | None = None, Q: float = 25.0, passband_ripple: float = 1, stopband_attenuation: float = 50, ftype: Literal["butter", "cheby1", "cheby2", "ellip"] = "ellip", flayout: Literal["ba", "sos", "zpk"] = "sos", ) -> tuple[list[Any], np.ndarray]: r"""Construct a multirate filterbank. A filter bank consists of multiple band-pass filters which divide the input signal into subbands. In the case of a multirate filter bank, the band-pass filters operate with resampled versions of the input signal, e.g. to keep the length of a filter constant while shifting its center frequency. This implementation uses `scipy.signal.iirdesign` to design the filters. Parameters ---------- center_freqs : np.ndarray [shape=(n,), dtype=float] Center frequencies of the filter kernels. Also defines the number of filters in the filterbank. sample_rates : np.ndarray [shape=(n,), dtype=float] Samplerate for each filter (used for multirate filterbank). Q : float Q factor (influences the filter bandwidth). passband_ripple : float The maximum loss in the passband (dB) See `scipy.signal.iirdesign` for details. stopband_attenuation : float The minimum attenuation in the stopband (dB) See `scipy.signal.iirdesign` for details. ftype : str The type of IIR filter to design See `scipy.signal.iirdesign` for details. flayout : str Valid `output` argument for `scipy.signal.iirdesign`. - If `ba`, returns numerators/denominators of the transfer functions, used for filtering with `scipy.signal.filtfilt`. Can be unstable for high-order filters. - If `sos`, returns a series of second-order filters, used for filtering with `scipy.signal.sosfiltfilt`. Minimizes numerical precision errors for high-order filters, but is slower. - If `zpk`, returns zeros, poles, and system gains of the transfer functions. Returns ------- filterbank : list [shape=(n,), dtype=float] Each list entry comprises the filter coefficients for a single filter. sample_rates : np.ndarray [shape=(n,), dtype=float] Samplerate for each filter. Notes ----- This function caches at level 10. See Also -------- scipy.signal.iirdesign Raises ------ ParameterError If ``center_freqs`` is ``None``. If ``sample_rates`` is ``None``. If ``center_freqs.shape`` does not match ``sample_rates.shape``. """ if center_freqs is None: raise ParameterError("center_freqs must be provided.") if sample_rates is None: raise ParameterError("sample_rates must be provided.") if center_freqs.shape != sample_rates.shape: raise ParameterError( "Number of provided center_freqs and sample_rates must be equal." ) nyquist = 0.5 * sample_rates filter_bandwidths = center_freqs / float(Q) filterbank = [] for cur_center_freq, cur_nyquist, cur_bw in zip( center_freqs, nyquist, filter_bandwidths, strict=True ): passband_freqs = [ cur_center_freq - 0.5 * cur_bw, cur_center_freq + 0.5 * cur_bw, ] / cur_nyquist stopband_freqs = [ cur_center_freq - cur_bw, cur_center_freq + cur_bw, ] / cur_nyquist import scipy.signal cur_filter = scipy.signal.iirdesign( passband_freqs, stopband_freqs, passband_ripple, stopband_attenuation, analog=False, ftype=ftype, output=flayout, ) filterbank.append(cur_filter) return filterbank, sample_rates @cache(level=10) def mr_frequencies(tuning: float) -> tuple[np.ndarray, np.ndarray]: r"""Generate center frequencies and sample rate pairs. This function will return center frequency and corresponding sample rates to obtain similar pitch filterbank settings as described in [#]_. Instead of starting with MIDI pitch `A0`, we start with `C0`. .. [#] Müller, Meinard. "Information Retrieval for Music and Motion." Springer Verlag. 2007. Parameters ---------- tuning : float [scalar] Tuning deviation from A440, measure as a fraction of the equally tempered semitone (1/12 of an octave). Returns ------- center_freqs : np.ndarray [shape=(n,), dtype=float] Center frequencies of the filter kernels. Also defines the number of filters in the filterbank. sample_rates : np.ndarray [shape=(n,), dtype=float] Sample rate for each filter, used for multirate filterbank. Notes ----- This function caches at level 10. See Also -------- librosa.filters.semitone_filterbank """ center_freqs = midi_to_hz(np.arange(24 + tuning, 109 + tuning)) sample_rates = np.asarray( len(np.arange(0, 36)) * [ 882.0, ] + len(np.arange(36, 70)) * [ 4410.0, ] + len(np.arange(70, 85)) * [ 22050.0, ] ) return center_freqs, sample_rates def semitone_filterbank( *, center_freqs: np.ndarray | None = None, tuning: float = 0.0, sample_rates: np.ndarray | None = None, flayout: Literal["ba", "sos"] = "ba", **kwargs: Any, ) -> tuple[list[Any], np.ndarray]: r"""Construct a multi-rate IIR band-pass filter bank at specified center frequencies and sample rates. By default, these center frequencies are set equal to the 88 fundamental frequencies of the grand piano keyboard, according to a pitch tuning standard of A440, that is, note A above middle C set to 440 Hz. The center frequencies are tuned to the twelve-tone equal temperament, which means that they grow exponentially at a rate of 2**(1/12), that is, twelve notes per octave. The A440 tuning can be changed by the user while keeping twelve-tone equal temperament. While A440 is currently the international standard in the music industry (ISO 16), some orchestras tune to A441-A445, whereas baroque musicians tune to A415. See [#]_ for details. .. [#] Müller, Meinard. "Information Retrieval for Music and Motion." Springer Verlag. 2007. Parameters ---------- center_freqs : np.ndarray [shape=(n,), dtype=float] Center frequencies of the filter kernels. Also defines the number of filters in the filterbank. tuning : float [scalar] Tuning deviation from A440 as a fraction of a semitone (1/12 of an octave in equal temperament). sample_rates : np.ndarray [shape=(n,), dtype=float] Sample rates of each filter in the multirate filterbank. flayout : str - If `ba`, the standard difference equation is used for filtering with `scipy.signal.filtfilt`. Can be unstable for high-order filters. - If `sos`, a series of second-order filters is used for filtering with `scipy.signal.sosfiltfilt`. Minimizes numerical precision errors for high-order filters, but is slower. **kwargs : additional keyword arguments Additional arguments to the private function `_multirate_fb()`. Returns ------- filterbank : list [shape=(n,), dtype=float] Each list entry contains the filter coefficients for a single filter. fb_sample_rates : np.ndarray [shape=(n,), dtype=float] Sample rate for each filter. See Also -------- librosa.cqt librosa.iirt librosa.filters.mr_frequencies scipy.signal.iirdesign Examples -------- >>> import matplotlib.pyplot as plt >>> import numpy as np >>> import scipy.signal >>> semitone_filterbank, sample_rates = librosa.filters.semitone_filterbank( ... center_freqs=librosa.midi_to_hz(np.arange(60, 72)), ... sample_rates=np.repeat(4410.0, 12), ... flayout='sos' ... ) >>> magnitudes = [] >>> for cur_sr, cur_filter in zip(sample_rates, semitone_filterbank): ... w, h = scipy.signal.sosfreqz(cur_filter,fs=cur_sr, worN=1025) ... magnitudes.append(20 * np.log10(np.abs(h))) >>> fig, ax = plt.subplots(figsize=(12,6)) >>> img = librosa.display.specshow( ... np.array(magnitudes), ... x_axis="hz", ... sr=4410, ... y_coords=librosa.midi_to_hz(np.arange(60, 72)), ... vmin=-60, ... vmax=3, ... ax=ax ... ) >>> librosa.display.colorbar_db(img) >>> ax.set( ... xlim=[200, 600], ... yticks=librosa.midi_to_hz(np.arange(60, 72)), ... title='Magnitude Responses of the Pitch Filterbank', ... xlabel='Frequency (Hz)', ... ylabel='Semitone filter center frequency (Hz)' ... ) """ if (center_freqs is None) and (sample_rates is None): center_freqs, sample_rates = mr_frequencies(tuning) filterbank, fb_sample_rates = _multirate_fb( center_freqs=center_freqs, sample_rates=sample_rates, flayout=flayout, **kwargs ) return filterbank, fb_sample_rates @jit(nopython=True, cache=True) def __window_ss_fill(x, win_sq, n_frames, hop_length): # pragma: no cover """Compute the sum-square envelope of a window.""" n = len(x) n_fft = len(win_sq) for i in range(n_frames): sample = i * hop_length x[sample : min(n, sample + n_fft)] += win_sq[: max(0, min(n_fft, n - sample))] def window_sumsquare( *, window: _WindowSpec, n_frames: int, hop_length: int = 512, win_length: int | None = None, n_fft: int = 2048, dtype: DTypeLike = np.float32, norm: float | None = None, ) -> np.ndarray: """Compute the sum-square envelope of a window function at a given hop length. This is used to estimate modulation effects induced by windowing observations in short-time Fourier transforms. Parameters ---------- window : str, tuple, number, callable, or list-like Window specification, as in `get_window` n_frames : int > 0 The number of analysis frames hop_length : int > 0 The number of samples to advance between frames win_length : [optional] The length of the window function. By default, this matches ``n_fft``. n_fft : int > 0 The length of each analysis frame. dtype : np.dtype The data type of the output norm : {np.inf, -np.inf, 0, float > 0, None} Normalization mode used in window construction. Note that this does not affect the squaring operation. Returns ------- wss : np.ndarray, shape=``(n_fft + hop_length * (n_frames - 1))`` The sum-squared envelope of the window function Examples -------- For a fixed frame length (2048), compare modulation effects for a Hann window at different hop lengths: >>> n_frames = 50 >>> wss_256 = librosa.filters.window_sumsquare(window='hann', n_frames=n_frames, hop_length=256) >>> wss_512 = librosa.filters.window_sumsquare(window='hann', n_frames=n_frames, hop_length=512) >>> wss_1024 = librosa.filters.window_sumsquare(window='hann', n_frames=n_frames, hop_length=1024) >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=3, sharey=True) >>> ax[0].plot(wss_256) >>> ax[0].set(title='hop_length=256') >>> ax[1].plot(wss_512) >>> ax[1].set(title='hop_length=512') >>> ax[2].plot(wss_1024) >>> ax[2].set(title='hop_length=1024') """ if win_length is None: win_length = n_fft n = n_fft + hop_length * (n_frames - 1) x = np.zeros(n, dtype=dtype) # Compute the squared window at the desired length win_sq = get_window(window, win_length) win_sq = util.normalize(win_sq, norm=norm) ** 2 win_sq = util.pad_center(win_sq, size=n_fft) # Fill the envelope __window_ss_fill(x, win_sq, n_frames, hop_length) return x @cache(level=10) def diagonal_filter( window: _WindowSpec, n: int, *, slope: float = 1.0, angle: float | None = None, zero_mean: bool = False, ) -> np.ndarray: """Build a two-dimensional diagonal filter. This is primarily used for smoothing recurrence or self-similarity matrices. Parameters ---------- window : str, tuple, number, callable, or list-like The window function to use for the filter. See `get_window` for details. Note that the window used here should be non-negative. n : int > 0 the length of the filter slope : float The slope of the diagonal filter to produce angle : float or None If given, the slope parameter is ignored, and angle directly sets the orientation of the filter (in radians). Otherwise, angle is inferred as `arctan(slope)`. zero_mean : bool If True, a zero-mean filter is used. Otherwise, a non-negative averaging filter is used. This should be enabled if you want to enhance paths and suppress blocks. Returns ------- kernel : np.ndarray, shape=[(m, m)] The 2-dimensional filter kernel Notes ----- This function caches at level 10. """ if angle is None: angle = np.arctan(slope) win: np.ndarray = np.diag(get_window(window, n, fftbins=False)) if not np.isclose(angle, np.pi / 4): import scipy.ndimage win = scipy.ndimage.rotate( win, 45 - angle * 180 / np.pi, order=5, prefilter=False ) np.clip(win, 0, None, out=win) win /= win.sum() if zero_mean: win -= win.mean() return win ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2125447 librosa-1.0.0/librosa/onset.py0000644000175100017510000005151015236640653015762 0ustar00runnerrunner#!/usr/bin/env python # -*- coding: utf-8 -*- """ librosa.onset contains functions for computing onset strength envelopes and estimating onset events from audio signals. """ from __future__ import annotations from typing import TYPE_CHECKING import numpy as np from . import core, util from ._cache import cache from .feature.spectral import melspectrogram from .util.exceptions import ParameterError if TYPE_CHECKING: from typing import Any, Callable, Sequence __all__ = ["onset_detect", "onset_strength", "onset_strength_multi", "onset_backtrack"] def onset_detect( *, y: np.ndarray | None = None, sr: float = 22050, onset_envelope: np.ndarray | None = None, hop_length: int = 512, backtrack: bool = False, energy: np.ndarray | None = None, units: str = "frames", normalize: bool = True, sparse: bool = True, **kwargs: Any, ) -> np.ndarray: """Locate note onset events by picking peaks in an onset strength envelope. The `peak_pick` parameters were chosen by large-scale hyper-parameter optimization over the dataset provided by [#]_. .. [#] https://github.com/CPJKU/onset_db Parameters ---------- y : np.ndarray [shape=(..., n)] audio time-series. Multi-channel is supported. sr : number > 0 [scalar] sampling rate of ``y`` onset_envelope : np.ndarray [shape=(..., m)] (optional) pre-computed onset strength envelope hop_length : int > 0 [scalar] hop length (in samples) backtrack : bool If ``True``, detected onset events are backtracked to the nearest preceding minimum of ``energy``. This is primarily useful when using onsets as slice points for segmentation. .. note:: backtracking is only supported if ``sparse=True``. energy : np.ndarray [shape=(m,)] (optional) An energy function to use for backtracking detected onset events. If none is provided, then ``onset_envelope`` is used. units : {'frames', 'samples', 'time'} The units to encode detected onset events in. By default, 'frames' are used. normalize : bool If ``True`` (default), normalize the onset envelope to have minimum of 0 and maximum of 1 prior to detection. This is helpful for standardizing the parameters of `librosa.util.peak_pick`. Otherwise, the onset envelope is left unnormalized. sparse : bool If ``True`` (default), detections are returned as an array of frames, samples, or time indices (as specified by ``units=``). If ``False``, detections are encoded as a dense boolean array where ``onsets[n]`` is True if there's an onset at frame index ``n``. .. note:: multi-channel input is only supported if ``sparse=False``. **kwargs : additional keyword arguments Additional parameters for peak picking. See `librosa.util.peak_pick` for details. Returns ------- onsets : np.ndarray [shape=(n_onsets,) or onset_envelope.shape] estimated positions of detected onsets, in whichever units are specified. By default, frame indices. If `sparse=False`, `onsets[..., n]` indicates an onset detection at frame index `n`. .. note:: If no onset strength could be detected, onset_detect returns an empty array (sparse=True) or all-False array (sparse=False). Raises ------ ParameterError if neither ``y`` nor ``onsets`` are provided or if ``units`` is not one of 'frames', 'samples', or 'time' See Also -------- onset_strength : Compute onset strength per-frame onset_backtrack : Backtracking onset events librosa.util.peak_pick : Pick peaks from a time series Examples -------- Get onset times from a signal >>> y, sr = librosa.loadx('trumpet') >>> librosa.onset.onset_detect(y=y, sr=sr, units='time') array([0.07 , 0.232, 0.395, 0.604, 0.743, 0.929, 1.045, 1.115, 1.416, 1.672, 1.881, 2.043, 2.206, 2.368, 2.554, 3.019]) Or use a pre-computed onset envelope >>> o_env = librosa.onset.onset_strength(y=y, sr=sr) >>> times = librosa.times_like(o_env, sr=sr) >>> onset_frames = librosa.onset.onset_detect(onset_envelope=o_env, sr=sr) >>> import matplotlib.pyplot as plt >>> D = np.abs(librosa.stft(y)) >>> fig, ax = plt.subplots(nrows=2, sharex=True) >>> librosa.display.specshow(D, vscale='dBFS', ... x_axis='time', y_axis='log', ax=ax[0], sr=sr) >>> ax[0].set(title='Power spectrogram') >>> ax[0].label_outer() >>> ax[1].plot(times, o_env, label='Onset strength') >>> ax[1].vlines(times[onset_frames], 0, o_env.max(), color='r', alpha=0.9, ... linestyle='--', label='Onsets') >>> ax[1].legend() """ # First, get the frame->beat strength profile if we don't already have one if onset_envelope is None: if y is None: raise ParameterError("y or onset_envelope must be provided") onset_envelope = onset_strength(y=y, sr=sr, hop_length=hop_length) # Shift onset envelope up to be non-negative # (a common normalization step to make the threshold more consistent) if normalize: # Normalize onset strength function to [0, 1] range # Normalization is performed over the trailing axis onset_envelope = onset_envelope - np.min(onset_envelope, keepdims=True, axis=-1) # Max-scale with safe division onset_envelope /= np.max(onset_envelope, keepdims=True, axis=-1) + util.tiny(onset_envelope) # help out mypy assert onset_envelope is not None # Do we have any onsets to grab? if not onset_envelope.any() or not np.all(np.isfinite(onset_envelope)): if sparse: onsets = np.array([], dtype=int) else: onsets = np.zeros_like(onset_envelope, dtype=bool) else: # These parameter settings found by large-scale search kwargs.setdefault("pre_max", 0.03 * sr // hop_length) # 30ms kwargs.setdefault("post_max", 0.00 * sr // hop_length + 1) # 0ms kwargs.setdefault("pre_avg", 0.10 * sr // hop_length) # 100ms kwargs.setdefault("post_avg", 0.10 * sr // hop_length + 1) # 100ms kwargs.setdefault("wait", 0.03 * sr // hop_length) # 30ms kwargs.setdefault("delta", 0.07) # Peak pick the onset envelope onsets = util.peak_pick(onset_envelope, sparse=sparse, axis=-1, **kwargs) # Optionally backtrack the events if backtrack: if not sparse: raise ParameterError("onset backtracking is only supported if sparse=True") if energy is None: energy = onset_envelope assert energy is not None onsets = onset_backtrack(onsets, energy) if sparse: if units == "frames": pass elif units == "samples": onsets = core.frames_to_samples(onsets, hop_length=hop_length) elif units == "time": onsets = core.frames_to_time(onsets, hop_length=hop_length, sr=sr) else: raise ParameterError(f"Invalid unit type: {units}") return onsets def onset_strength( *, y: np.ndarray | None = None, sr: float = 22050, S: np.ndarray | None = None, lag: int = 1, max_size: int = 1, ref: np.ndarray | None = None, detrend: bool = False, center: bool = True, feature: Callable | None = None, aggregate: Callable | bool | None = None, **kwargs: Any, ) -> np.ndarray: """Compute a spectral flux onset strength envelope. Onset strength at time ``t`` is determined by:: mean_f max(0, S[f, t] - ref[f, t - lag]) where ``ref`` is ``S`` after local max filtering along the frequency axis [#]_. By default, if a time series ``y`` is provided, S will be the log-power Mel spectrogram. .. [#] Böck, Sebastian, and Gerhard Widmer. "Maximum filter vibrato suppression for onset detection." 16th International Conference on Digital Audio Effects, Maynooth, Ireland. 2013. Parameters ---------- y : np.ndarray [shape=(..., n)] audio time-series. Multi-channel is supported. sr : number > 0 [scalar] sampling rate of ``y`` S : np.ndarray [shape=(..., d, m)] pre-computed (log-power) spectrogram lag : int > 0 time lag for computing differences max_size : int > 0 size (in frequency bins) of the local max filter. set to `1` to disable filtering. ref : None or np.ndarray [shape=(..., d, m)] An optional pre-computed reference spectrum, of the same shape as ``S``. If not provided, it will be computed from ``S``. If provided, it will override any local max filtering governed by ``max_size``. detrend : bool [scalar] Filter the onset strength to remove the DC component center : bool [scalar] Shift the onset function by ``n_fft // (2 * hop_length)`` frames. This corresponds to using a centered frame analysis in the short-time Fourier transform. feature : function Function for computing time-series features, eg, scaled spectrograms. By default, uses `librosa.feature.melspectrogram` with ``fmax=sr/2`` aggregate : function Aggregation function to use when combining onsets at different frequency bins. Default: `np.mean` **kwargs : additional keyword arguments Additional parameters to ``feature()``, if ``S`` is not provided. Returns ------- onset_envelope : np.ndarray [shape=(..., m,)] vector containing the onset strength envelope. If the input contains multiple channels, then onset envelope is computed for each channel. Raises ------ ParameterError if neither ``(y, sr)`` nor ``S`` are provided or if ``lag`` or ``max_size`` are not positive integers See Also -------- onset_detect onset_strength_multi Examples -------- First, load some audio and plot the spectrogram >>> import matplotlib.pyplot as plt >>> y, sr = librosa.loadx('trumpet', duration=3) >>> D = np.abs(librosa.stft(y)) >>> times = librosa.times_like(D, sr=sr) >>> fig, ax = plt.subplots(nrows=2, sharex=True) >>> librosa.display.specshow(D, vscale='dBFS', ... y_axis='log', x_axis='time', ax=ax[0], sr=sr) >>> ax[0].set(title='Power spectrogram') >>> ax[0].label_outer() Construct a standard onset function >>> onset_env = librosa.onset.onset_strength(y=y, sr=sr) >>> ax[1].plot(times, 2 + onset_env / onset_env.max(), alpha=0.8, ... label='Mean (mel)') Median aggregation, and custom mel options >>> onset_env = librosa.onset.onset_strength(y=y, sr=sr, ... aggregate=np.median, ... fmax=8000, n_mels=256) >>> ax[1].plot(times, 1 + onset_env / onset_env.max(), alpha=0.8, ... label='Median (custom mel)') Constant-Q spectrogram instead of Mel >>> C = np.abs(librosa.cqt(y=y, sr=sr)) >>> onset_env = librosa.onset.onset_strength(sr=sr, S=librosa.amplitude_to_db(C, ref=np.max)) >>> ax[1].plot(times, onset_env / onset_env.max(), alpha=0.8, ... label='Mean (CQT)') >>> ax[1].legend() >>> ax[1].set(ylabel='Normalized strength', yticks=[]) """ if aggregate is False: raise ParameterError( "aggregate parameter cannot be False when computing full-spectrum onset strength." ) odf_all = onset_strength_multi( y=y, sr=sr, S=S, lag=lag, max_size=max_size, ref=ref, detrend=detrend, center=center, feature=feature, aggregate=aggregate, channels=None, **kwargs, ) return odf_all[..., 0, :] def onset_backtrack(events: np.ndarray, energy: np.ndarray) -> np.ndarray: """Backtrack detected onset events to the nearest preceding local minimum of an energy function. This function can be used to roll back the timing of detected onsets from a detected peak amplitude to the preceding minimum. This is most useful when using onsets to determine slice points for segmentation, as described by [#]_. .. [#] Jehan, Tristan. "Creating music by listening" Doctoral dissertation Massachusetts Institute of Technology, 2005. Parameters ---------- events : np.ndarray, dtype=int List of onset event frame indices, as computed by `onset_detect` energy : np.ndarray, shape=(m,) An energy function Returns ------- events_backtracked : np.ndarray, shape=events.shape The input events matched to nearest preceding minima of ``energy``. Examples -------- Backtrack the events using the onset envelope >>> y, sr = librosa.loadx('trumpet', duration=3) >>> oenv = librosa.onset.onset_strength(y=y, sr=sr) >>> times = librosa.times_like(oenv, sr=sr) >>> # Detect events without backtracking >>> onset_raw = librosa.onset.onset_detect(onset_envelope=oenv, ... backtrack=False) >>> onset_bt = librosa.onset.onset_backtrack(onset_raw, oenv) Backtrack the events using the RMS values >>> S = np.abs(librosa.stft(y=y)) >>> rms = librosa.feature.rms(S=S) >>> onset_bt_rms = librosa.onset.onset_backtrack(onset_raw, rms[0]) Plot the results >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=3, sharex=True) >>> librosa.display.specshow(S, vscale='dBFS', ... y_axis='log', x_axis='time', ax=ax[0]) >>> ax[0].label_outer() >>> ax[1].plot(times, oenv, label='Onset strength') >>> ax[1].vlines(librosa.frames_to_time(onset_raw), 0, oenv.max(), label='Raw onsets') >>> ax[1].vlines(librosa.frames_to_time(onset_bt), 0, oenv.max(), label='Backtracked', color='r') >>> ax[1].legend() >>> ax[1].label_outer() >>> ax[2].plot(times, rms[0], label='RMS') >>> ax[2].vlines(librosa.frames_to_time(onset_bt_rms), 0, rms.max(), label='Backtracked (RMS)', color='r') >>> ax[2].legend() """ # Find points where energy is non-increasing # all points: energy[i] <= energy[i-1] # tail points: energy[i] < energy[i+1] minima = np.flatnonzero((energy[1:-1] <= energy[:-2]) & (energy[1:-1] < energy[2:])) # Pad on a 0, just in case we have onsets with no preceding minimum # Shift by one to account for slicing in minima detection minima = util.fix_frames(1 + minima, x_min=0) # Only match going left from the detected events results: np.ndarray = minima[util.match_events(events, minima, right=False)] return results @cache(level=30) def onset_strength_multi( *, y: np.ndarray | None = None, sr: float = 22050, S: np.ndarray | None = None, n_fft: int = 2048, hop_length: int = 512, lag: int = 1, max_size: int = 1, ref: np.ndarray | None = None, detrend: bool = False, center: bool = True, feature: Callable | None = None, aggregate: Callable | bool | None = None, channels: Sequence[int] | Sequence[slice] | None = None, **kwargs: Any, ) -> np.ndarray: """Compute a spectral flux onset strength envelope across multiple channels. Onset strength for channel ``i`` at time ``t`` is determined by:: mean_{f in channels[i]} max(0, S[f, t+1] - S[f, t]) Parameters ---------- y : np.ndarray [shape=(..., n,)] audio time-series. Multi-channel is supported. sr : number > 0 [scalar] sampling rate of ``y`` S : np.ndarray [shape=(..., d, m)] pre-computed (log-power) spectrogram n_fft : int > 0 [scalar] length of the FFT frame for use in ``feature()`` if ``S`` is not provided. hop_length : int > 0 [scalar] hop length for use in ``feature()`` if ``S`` is not provided. lag : int > 0 time lag for computing differences max_size : int > 0 size (in frequency bins) of the local max filter. set to `1` to disable filtering. ref : None or np.ndarray [shape=(d, m)] An optional pre-computed reference spectrum, of the same shape as ``S``. If not provided, it will be computed from ``S``. If provided, it will override any local max filtering governed by ``max_size``. detrend : bool [scalar] Filter the onset strength to remove the DC component center : bool [scalar] Shift the onset function by ``n_fft // (2 * hop_length)`` frames. This corresponds to using a centered frame analysis in the short-time Fourier transform. feature : function Function for computing time-series features, eg, scaled spectrograms. By default, uses `librosa.feature.melspectrogram` with ``fmax=sr/2`` Must support arguments: ``y, sr, n_fft, hop_length`` aggregate : function or False Aggregation function to use when combining onsets at different frequency bins. If ``False``, then no aggregation is performed. Default: `np.mean` channels : list or None Array of channel boundaries or slice objects. If `None`, then a single channel is generated to span all bands. **kwargs : additional keyword arguments Additional parameters to ``feature()``, if ``S`` is not provided. Returns ------- onset_envelope : np.ndarray [shape=(..., n_channels, m)] array containing the onset strength envelope for each specified channel Raises ------ ParameterError if neither ``(y, sr)`` nor ``S`` are provided See Also -------- onset_strength Notes ----- This function caches at level 30. Examples -------- First, load some audio and plot the spectrogram >>> import matplotlib.pyplot as plt >>> y, sr = librosa.loadx('choice', duration=5) >>> D = np.abs(librosa.stft(y)) >>> fig, ax = plt.subplots(nrows=2, sharex=True) >>> img1 = librosa.display.specshow(D, vscale='dBFS', ... y_axis='log', x_axis='time', ax=ax[0]) >>> ax[0].set(title='Power spectrogram') >>> ax[0].label_outer() >>> librosa.display.colorbar_db(img1, ax=ax[0]) Construct a standard onset function over four sub-bands >>> onset_subbands = librosa.onset.onset_strength_multi(y=y, sr=sr, ... channels=[0, 32, 64, 96, 128]) >>> img2 = librosa.display.specshow(onset_subbands, x_axis='time', ax=ax[1]) >>> ax[1].set(ylabel='Sub-bands', title='Sub-band onset strength') >>> fig.colorbar(img2, ax=[ax[1]]) """ if feature is None: feature = melspectrogram kwargs.setdefault("fmax", 0.5 * sr) if aggregate is None: aggregate = np.mean if not util.is_positive_int(lag): raise ParameterError(f"lag={lag} must be a positive integer") if not util.is_positive_int(max_size): raise ParameterError(f"max_size={max_size} must be a positive integer") # First, compute mel spectrogram if S is None: S = np.abs(feature(y=y, sr=sr, n_fft=n_fft, hop_length=hop_length, **kwargs)) # Convert to dBs S = core.power_to_db(S) # Assertion to make type checking happy assert S is not None # Ensure that S is at least 2-d S = np.atleast_2d(S) # Compute the reference spectrogram. # Efficiency hack: skip filtering step and pass by reference # if max_size will produce a no-op. if ref is None: if max_size == 1: ref = S else: import scipy.ndimage ref = scipy.ndimage.maximum_filter1d(S, max_size, axis=-2) elif ref.shape != S.shape: raise ParameterError( f"Reference spectrum shape {ref.shape} must match input spectrum {S.shape}" ) # Compute difference to the reference, spaced by lag onset_env = S[..., lag:] - ref[..., :-lag] # Discard negatives (decreasing amplitude) onset_env = np.maximum(0.0, onset_env) # Aggregate within channels pad = True if channels is None: channels = [slice(None)] else: pad = False if callable(aggregate): onset_env = util.sync( onset_env, channels, aggregate=aggregate, pad=pad, axis=-2 ) # compensate for lag pad_width = lag if center: # Counter-act framing effects. Shift the onsets by n_fft / hop_length pad_width += n_fft // (2 * hop_length) padding = [(0, 0) for _ in onset_env.shape] padding[-1] = (int(pad_width), 0) onset_env = np.pad(onset_env, padding, mode="constant") # remove the DC component if detrend: import scipy.signal onset_env = scipy.signal.lfilter([1.0, -1.0], [1.0, -0.99], onset_env, axis=-1) # Trim to match the input duration if center: onset_env = onset_env[..., : S.shape[-1]] return onset_env ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2125447 librosa-1.0.0/librosa/py.typed0000644000175100017510000000000015236640653015743 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2125447 librosa-1.0.0/librosa/segment.py0000644000175100017510000014510315236640653016276 0ustar00runnerrunner#!/usr/bin/env python # -*- coding: utf-8 -*- """ librosa.segment provides functions for structural analysis and segmentation of audio. This includes recurrence (self-similarity) and cross-similarity matrix computation, filtering and transformations of similarity matrices, and temporal segmentation by clustering. """ from __future__ import annotations import itertools from typing import TYPE_CHECKING, cast, overload import numpy as np import scipy from decorator import decorator from . import util from ._cache import cache from .filters import diagonal_filter from .util.exceptions import ParameterError if TYPE_CHECKING: from typing import Any, Callable, Literal, TypeVar import sklearn.cluster from ._typing import _Array1D, _FloatLike_co, _SparseArray, _WindowSpec _ArrayOrSparseArray = TypeVar( "_ArrayOrSparseArray", bound=np.ndarray | _SparseArray ) __all__ = [ "cross_similarity", "recurrence_matrix", "recurrence_to_lag", "lag_to_recurrence", "timelag_filter", "agglomerative", "subsegment", "path_enhance", ] @overload def cross_similarity( data: np.ndarray, data_ref: np.ndarray, *, k: int | None = ..., metric: str = ..., sparse: Literal[False] = ..., mode: str = ..., bandwidth: np.ndarray | _FloatLike_co | str | None = None, full: bool = False, ) -> np.ndarray: ... @overload def cross_similarity( data: np.ndarray, data_ref: np.ndarray, *, k: int | None = ..., metric: str = ..., sparse: Literal[True], mode: str = ..., bandwidth: np.ndarray | _FloatLike_co | str | None = None, full: bool = False, ) -> scipy.sparse.csc_array: ... @cache(level=30) def cross_similarity( data: np.ndarray, data_ref: np.ndarray, *, k: int | None = None, metric: str = "euclidean", sparse: bool = False, mode: str = "connectivity", bandwidth: np.ndarray | _FloatLike_co | str | None = None, full: bool = False, ) -> np.ndarray | scipy.sparse.csc_array: """Compute cross-similarity from one data sequence to a reference sequence. The output is a matrix ``xsim``, where ``xsim[i, j]`` is non-zero if ``data_ref[..., i]`` is a k-nearest neighbor of ``data[..., j]``. Parameters ---------- data : np.ndarray [shape=(..., d, n)] A feature matrix for the comparison sequence. If the data has more than two dimensions (e.g., for multi-channel inputs), the leading dimensions are flattened prior to comparison. For example, a stereo input with shape `(2, d, n)` is automatically reshaped to `(2 * d, n)`. data_ref : np.ndarray [shape=(..., d, n_ref)] A feature matrix for the reference sequence If the data has more than two dimensions (e.g., for multi-channel inputs), the leading dimensions are flattened prior to comparison. For example, a stereo input with shape `(2, d, n_ref)` is automatically reshaped to `(2 * d, n_ref)`. k : int > 0 [scalar] or None the number of nearest-neighbors for each sample Default: ``k = 2 * ceil(sqrt(n_ref))``, or ``k = 2`` if ``n_ref <= 3`` metric : str Distance metric to use for nearest-neighbor calculation. See `sklearn.neighbors.NearestNeighbors` for details. sparse : bool [scalar] if False, returns a dense type (ndarray) if True, returns a sparse type (scipy.sparse.csc_array) mode : str, {'connectivity', 'distance', 'affinity'} If 'connectivity', a binary connectivity matrix is produced. If 'distance', then a non-zero entry contains the distance between points. If 'affinity', then non-zero entries are mapped to ``exp( - distance(i, j) / bandwidth)`` where ``bandwidth`` is as specified below. bandwidth : None, float > 0, ndarray, or str str options include - 'med_k_scalar' - 'mean_k' - 'gmean_k' - 'mean_k_avg' - 'gmean_k_avg' - 'mean_k_avg_and_pair' If ndarray is supplied, use ndarray as bandwidth for each i,j pair. If using ``mode='affinity'``, this can be used to set the bandwidth on the affinity kernel. If no value is provided or ``None``, default to ``'med_k_scalar'``. If ``bandwidth='med_k_scalar'``, bandwidth is set automatically to the median distance to the k'th nearest neighbor of each ``data[:, i]``. If ``bandwidth='mean_k'``, bandwidth is estimated for each sample-pair (i, j) by taking the arithmetic mean between distances to the k-th nearest neighbor for sample i and sample j. If ``bandwidth='gmean_k'``, bandwidth is estimated for each sample-pair (i, j) by taking the geometric mean between distances to the k-th nearest neighbor for sample i and j [#z]_. If ``bandwidth='mean_k_avg'``, bandwidth is estimated for each sample-pair (i, j) by taking the arithmetic mean between the average distances to the first k-th nearest neighbors for sample i and sample j. This is similar to the approach in Wang et al. (2014) [#w]_ but does not include the distance between i and j. If ``bandwidth='gmean_k_avg'``, bandwidth is estimated for each sample-pair (i, j) by taking the geometric mean between the average distances to the first k-th nearest neighbors for sample i and sample j. If ``bandwidth='mean_k_avg_and_pair'``, bandwidth is estimated for each sample-pair (i, j) by taking the arithmetic mean between three terms: the average distances to the first k-th nearest neighbors for sample i and sample j respectively, as well as the distance between i and j. This is similar to the approach in Wang et al. (2014). [#w]_ .. [#z] Zelnik-Manor, Lihi, and Pietro Perona. (2004). "Self-tuning spectral clustering." Advances in neural information processing systems 17. .. [#w] Wang, Bo, et al. (2014). "Similarity network fusion for aggregating data types on a genomic scale." Nat Methods 11, 333--337. https://doi.org/10.1038/nmeth.2810 full : bool If using ``mode ='affinity'`` or ``mode='distance'``, this option can be used to compute the full affinity or distance matrix as opposed a sparse matrix with only none-zero terms for the first k-neighbors of each sample. This option has no effect when using ``mode='connectivity'``. When using ``mode='distance'``, setting ``full=True`` will ignore ``k`` and ``width``. When using ``mode='affinity'``, setting ``full=True`` will use ``k`` exclusively for bandwidth estimation, and ignore ``width``. Returns ------- xsim : np.ndarray or scipy.sparse.csc_array, [shape=(n_ref, n)] Cross-similarity matrix See Also -------- recurrence_matrix recurrence_to_lag librosa.feature.stack_memory sklearn.neighbors.NearestNeighbors scipy.spatial.distance.cdist Notes ----- This function caches at level 30. Examples -------- Find nearest neighbors in CQT space between two sequences >>> hop_length = 1024 >>> y_ref, sr = librosa.loadx('pistachio') >>> y_comp, sr = librosa.loadx('pistachio', offset=10) >>> chroma_ref = librosa.feature.chroma_cqt(y=y_ref, sr=sr, hop_length=hop_length) >>> chroma_comp = librosa.feature.chroma_cqt(y=y_comp, sr=sr, hop_length=hop_length) >>> # Use time-delay embedding to get a cleaner recurrence matrix >>> x_ref = librosa.feature.stack_memory(chroma_ref, n_steps=10, delay=3) >>> x_comp = librosa.feature.stack_memory(chroma_comp, n_steps=10, delay=3) >>> xsim = librosa.segment.cross_similarity(x_comp, x_ref) Or fix the number of nearest neighbors to 5 >>> xsim = librosa.segment.cross_similarity(x_comp, x_ref, k=5) Use cosine similarity instead of Euclidean distance >>> xsim = librosa.segment.cross_similarity(x_comp, x_ref, metric='cosine') Use an affinity matrix instead of binary connectivity >>> xsim_aff = librosa.segment.cross_similarity(x_comp, x_ref, metric='cosine', mode='affinity') Plot the feature and recurrence matrices >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(ncols=2, sharex=True, sharey=True) >>> imgsim = librosa.display.specshow(xsim, x_axis='s', y_axis='s', ... hop_length=hop_length, ax=ax[0]) >>> ax[0].set(title='Binary cross-similarity (symmetric)') >>> imgaff = librosa.display.specshow(xsim_aff, x_axis='s', y_axis='s', ... cmap='magma_r', hop_length=hop_length, ax=ax[1]) >>> ax[1].set(title='Cross-affinity') >>> ax[1].label_outer() >>> fig.colorbar(imgsim, ax=ax[0], orientation='horizontal', ticks=[0, 1]) >>> fig.colorbar(imgaff, ax=ax[1], orientation='horizontal') """ data_ref = np.atleast_2d(data_ref) data = np.atleast_2d(data) if not np.allclose(data_ref.shape[:-1], data.shape[:-1]): raise ParameterError( f"data_ref.shape={data_ref.shape} and data.shape={data.shape} do not match on leading dimension(s)" ) # swap data axes so the feature axis is last data_ref = np.swapaxes(data_ref, -1, 0) n_ref = data_ref.shape[0] # Use F-ordering for reshape to preserve leading axis data_ref = data_ref.reshape((n_ref, -1), order="F") data = np.swapaxes(data, -1, 0) n = data.shape[0] data = data.reshape((n, -1), order="F") if mode not in ["connectivity", "distance", "affinity"]: raise ParameterError( ( f"Invalid mode='{mode}'. Must be one of " "['connectivity', 'distance', 'affinity']" ) ) if k is None: k = min(n_ref, 2 * np.ceil(np.sqrt(n_ref))) k = int(k) # using k for bandwidth estimation also and decouple k for full mode bandwidth_k = k if full and (mode != "connectivity"): k = n # Build the neighbor search object # `auto` mode does not work with some choices of metric. Rather than special-case # those here, we instead use a fall-back to brute force if auto fails. import sklearn.neighbors try: knn = sklearn.neighbors.NearestNeighbors( n_neighbors=min(n_ref, k), metric=metric, algorithm="auto" ) except ValueError: knn = sklearn.neighbors.NearestNeighbors( n_neighbors=min(n_ref, k), metric=metric, algorithm="brute" ) knn.fit(data_ref) # Get the knn graph if mode == "affinity": # sklearn's nearest neighbor doesn't support affinity, # so we use distance here and then do the conversion post-hoc kng_mode = "distance" else: kng_mode = mode xsim = knn.kneighbors_graph(X=data, mode=kng_mode).tolil() if not full: # Retain only the top-k links per point for i in range(n): # Get the links from point i links = xsim[i].nonzero()[1] # Order them ascending idx = links[np.argsort(xsim[i, links].toarray())][0] # Everything past the kth closest gets squashed xsim[i, idx[k:]] = 0 # Convert to CSR xsim = xsim.tocsr() xsim.eliminate_zeros() # sklearn gave us a sparse *matrix*; normalize to sparse *array* xsim = scipy.sparse.csr_array(xsim) if mode == "connectivity": xsim = xsim.astype(bool) elif mode == "affinity": aff_bandwidth = __affinity_bandwidth(xsim, bandwidth, bandwidth_k) xsim.data[:] = np.exp(xsim.data / (-1 * aff_bandwidth)) # Transpose to n_ref by n and guarantee sparse *array* return type xsim = scipy.sparse.csc_array(xsim.T) if not sparse: return xsim.toarray() return xsim @overload def recurrence_matrix( data: np.ndarray, *, k: int | None = ..., width: int = ..., metric: str = ..., sym: bool = ..., sparse: Literal[True], mode: str = ..., bandwidth: np.ndarray | _FloatLike_co | str | None = ..., self: bool = ..., axis: int = ..., full: bool = False, ) -> scipy.sparse.csc_array: ... @overload def recurrence_matrix( data: np.ndarray, *, k: int | None = ..., width: int = ..., metric: str = ..., sym: bool = ..., sparse: Literal[False] = ..., mode: str = ..., bandwidth: np.ndarray | _FloatLike_co | str | None = ..., self: bool = ..., axis: int = ..., full: bool = False, ) -> np.ndarray: ... @cache(level=30) def recurrence_matrix( data: np.ndarray, *, k: int | None = None, width: int = 1, metric: str = "euclidean", sym: bool = False, sparse: bool = False, mode: str = "connectivity", bandwidth: np.ndarray | _FloatLike_co | str | None = None, self: bool = False, axis: int = -1, full: bool = False, ) -> np.ndarray | scipy.sparse.csc_array: """Compute a recurrence matrix from a data matrix. ``rec[i, j]`` is non-zero if ``data[..., i]`` is a k-nearest neighbor of ``data[..., j]`` and ``|i - j| >= width`` The specific value of ``rec[i, j]`` can have several forms, governed by the ``mode`` parameter below: - Connectivity: ``rec[i, j] = 1 or 0`` indicates that frames ``i`` and ``j`` are repetitions - Affinity: ``rec[i, j] > 0`` measures how similar frames ``i`` and ``j`` are. This is also known as a (sparse) self-similarity matrix. - Distance: ``rec[i, j] > 0`` measures how distant frames ``i`` and ``j`` are. This is also known as a (sparse) self-distance matrix. The general term *recurrence matrix* can refer to any of the three forms above. Parameters ---------- data : np.ndarray [shape=(..., d, n)] A feature matrix. If the data has more than two dimensions (e.g., for multi-channel inputs), the leading dimensions are flattened prior to comparison. For example, a stereo input with shape `(2, d, n)` is automatically reshaped to `(2 * d, n)`. k : int > 0 [scalar] or None the number of nearest-neighbors for each sample Default: ``k = 2 * ceil(sqrt(t - 2 * width + 1))``, or ``k = 2`` if ``t <= 2 * width + 1`` width : int >= 1 [scalar] only link neighbors ``(data[..., i], data[..., j])`` if ``|i - j| >= width`` ``width`` cannot exceed the length of the data. metric : str Distance metric to use for nearest-neighbor calculation. See `sklearn.neighbors.NearestNeighbors` for details. sym : bool [scalar] set ``sym=True`` to only link mutual nearest-neighbors sparse : bool [scalar] if False, returns a dense type (ndarray) if True, returns a sparse type (scipy.sparse.csc_array) mode : str, {'connectivity', 'distance', 'affinity'} If 'connectivity', a binary connectivity matrix is produced. If 'distance', then a non-zero entry contains the distance between points. If 'affinity', then non-zero entries are mapped to ``exp( - distance(i, j) / bandwidth)`` where ``bandwidth`` is as specified below. bandwidth : None, float > 0, ndarray, or str str options include: - 'med_k_scalar' - 'mean_k' - 'gmean_k' - 'mean_k_avg' - 'gmean_k_avg' - 'mean_k_avg_and_pair' If ndarray is supplied, use ndarray as bandwidth for each i,j pair. If using ``mode='affinity'``, the ``bandwidth`` option can be used to set the bandwidth on the affinity kernel. If no value is provided or ``None``, default to ``'med_k_scalar'``. If ``bandwidth='med_k_scalar'``, a scalar bandwidth is set to the median distance of the k-th nearest neighbor for all samples. If ``bandwidth='mean_k'``, bandwidth is estimated for each sample-pair (i, j) by taking the arithmetic mean between distances to the k-th nearest neighbor for sample i and sample j. If ``bandwidth='gmean_k'``, bandwidth is estimated for each sample-pair (i, j) by taking the geometric mean between distances to the k-th nearest neighbor for sample i and j [#z]_. If ``bandwidth='mean_k_avg'``, bandwidth is estimated for each sample-pair (i, j) by taking the arithmetic mean between the average distances to the first k-th nearest neighbors for sample i and sample j. This is similar to the approach in Wang et al. (2014) [#w]_ but does not include the distance between i and j. If ``bandwidth='gmean_k_avg'``, bandwidth is estimated for each sample-pair (i, j) by taking the geometric mean between the average distances to the first k-th nearest neighbors for sample i and sample j. If ``bandwidth='mean_k_avg_and_pair'``, bandwidth is estimated for each sample-pair (i, j) by taking the arithmetic mean between three terms: the average distances to the first k-th nearest neighbors for sample i and sample j respectively, as well as the distance between i and j. This is similar to the approach in Wang et al. (2014). [#w]_ .. [#z] Zelnik-Manor, Lihi, and Pietro Perona. (2004). "Self-tuning spectral clustering." Advances in neural information processing systems 17. .. [#w] Wang, Bo, et al. (2014). "Similarity network fusion for aggregating data types on a genomic scale." Nat Methods 11, 333--337. https://doi.org/10.1038/nmeth.2810 self : bool If ``True``, then the main diagonal is populated with self-links: 0 if ``mode='distance'``, and 1 otherwise. If ``False``, the main diagonal is left empty. axis : int The axis along which to compute recurrence. By default, the last index (-1) is taken. full : bool If using ``mode ='affinity'`` or ``mode='distance'``, this option can be used to compute the full affinity or distance matrix as opposed a sparse matrix with only none-zero terms for the first k-neighbors of each sample. This option has no effect when using ``mode='connectivity'``. When using ``mode='distance'``, setting ``full=True`` will ignore ``k`` and ``width``. When using ``mode='affinity'``, setting ``full=True`` will use ``k`` exclusively for bandwidth estimation, and ignore ``width``. Returns ------- rec : np.ndarray or scipy.sparse.csc_array, [shape=(t, t)] Recurrence matrix See Also -------- sklearn.neighbors.NearestNeighbors scipy.spatial.distance.cdist librosa.feature.stack_memory recurrence_to_lag Notes ----- This function caches at level 30. Examples -------- Find nearest neighbors in CQT space >>> y, sr = librosa.loadx('nutcracker') >>> hop_length = 1024 >>> chroma = librosa.feature.chroma_cqt(y=y, sr=sr, hop_length=hop_length) >>> # Use time-delay embedding to get a cleaner recurrence matrix >>> chroma_stack = librosa.feature.stack_memory(chroma, n_steps=10, delay=3) >>> R = librosa.segment.recurrence_matrix(chroma_stack) Or fix the number of nearest neighbors to 5 >>> R = librosa.segment.recurrence_matrix(chroma_stack, k=5) Suppress neighbors within +- 7 frames >>> R = librosa.segment.recurrence_matrix(chroma_stack, width=7) Use cosine similarity instead of Euclidean distance >>> R = librosa.segment.recurrence_matrix(chroma_stack, metric='cosine') Require mutual nearest neighbors >>> R = librosa.segment.recurrence_matrix(chroma_stack, sym=True) Use an affinity matrix instead of binary connectivity >>> R_aff = librosa.segment.recurrence_matrix(chroma_stack, metric='cosine', ... mode='affinity') Plot the feature and recurrence matrices >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(ncols=2, sharex=True, sharey=True) >>> imgsim = librosa.display.specshow(R, x_axis='s', y_axis='s', ... hop_length=hop_length, ax=ax[0]) >>> ax[0].set(title='Binary recurrence (symmetric)') >>> imgaff = librosa.display.specshow(R_aff, x_axis='s', y_axis='s', ... hop_length=hop_length, cmap='magma_r', ax=ax[1]) >>> ax[1].set(title='Affinity recurrence') >>> ax[1].label_outer() >>> fig.colorbar(imgsim, ax=ax[0], orientation='horizontal', ticks=[0, 1]) >>> fig.colorbar(imgaff, ax=ax[1], orientation='horizontal') """ data = np.atleast_2d(data) # Swap observations to the first dimension and flatten the rest data = np.swapaxes(data, axis, 0) t = data.shape[0] # Use F-ordering here to preserve leading axis layout data = data.reshape((t, -1), order="F") if width < 1 or width >= (t - 1) // 2: raise ParameterError( "width={} must be at least 1 and at most (data.shape[{}] - 1) // 2={}".format( width, axis, (t - 1) // 2 ) ) if mode not in ["connectivity", "distance", "affinity"]: raise ParameterError( ( f"Invalid mode='{mode}'. Must be one of " "['connectivity', 'distance', 'affinity']" ) ) if k is None: k = 2 * np.ceil(np.sqrt(t - 2 * width + 1)) k = int(k) # using k for bandwidth estimation also and decouple k for full mode bandwidth_k = k if full and (mode != "connectivity"): k = t # Build the neighbor search object import sklearn.neighbors try: knn = sklearn.neighbors.NearestNeighbors( n_neighbors=min(t - 1, k + 2 * width), metric=metric, algorithm="auto" ) except ValueError: knn = sklearn.neighbors.NearestNeighbors( n_neighbors=min(t - 1, k + 2 * width), metric=metric, algorithm="brute" ) knn.fit(data) # Get the knn graph if mode == "affinity": kng_mode = "distance" else: kng_mode = mode rec = knn.kneighbors_graph(mode=kng_mode).tolil() if not full: # Remove connections within width for diag in range(-width + 1, width): rec.setdiag(0, diag) # Retain only the top-k links per point for i in range(t): # Get the links from point i links = rec[i].nonzero()[1] # Order them ascending idx = links[np.argsort(rec[i, links].toarray())][0] # Everything past the kth closest gets squashed rec[i, idx[k:]] = 0 if self: if mode == "connectivity": rec.setdiag(1) elif mode == "affinity": # we need to keep the self-loop in here, but not mess up the # bandwidth estimation # # using negative distances here preserves the structure without changing # the statistics of the data rec.setdiag(-1) else: rec.setdiag(0) # symmetrize if sym: # Note: this operation produces a CSR (compressed sparse row) matrix! # This is why we have to do it after filling the diagonal in self-mode rec = rec.minimum(rec.T) # Convert to CSR (matrix), then normalize to sparse *array* rec = rec.tocsr() rec.eliminate_zeros() rec = scipy.sparse.csr_array(rec) if mode == "connectivity": rec = rec.astype(bool) elif mode == "affinity": # Set all the negatives back to 0 # Negatives are temporarily inserted above to preserve the sparsity structure # of the matrix without corrupting the bandwidth calculations rec.data[rec.data < 0] = 0.0 aff_bandwidth = __affinity_bandwidth(rec, bandwidth, bandwidth_k) rec.data[:] = np.exp(rec.data / (-1 * aff_bandwidth)) # Transpose to be column-major rec = scipy.sparse.csc_array(rec.T) if not sparse: return rec.toarray() return rec def recurrence_to_lag( rec: _ArrayOrSparseArray, *, pad: bool = True, axis: int = -1 ) -> _ArrayOrSparseArray: """Convert a recurrence matrix into a lag matrix. ``lag[i, j] == rec[i+j, j]`` This transformation turns diagonal structures in the recurrence matrix into horizontal structures in the lag matrix. These horizontal structures can be used to infer changes in the repetition structure of a piece, e.g., the beginning of a new section as done in [#]_. .. [#] Serra, J., Müller, M., Grosche, P., & Arcos, J. L. (2014). Unsupervised music structure annotation by time series structure features and segment similarity. IEEE Transactions on Multimedia, 16(5), 1229-1240. Parameters ---------- rec : np.ndarray, or scipy.sparse array [shape=(n, n)] A (binary) recurrence matrix, as returned by `recurrence_matrix` pad : bool If False, ``lag`` matrix is square, which is equivalent to assuming that the signal repeats itself indefinitely. If True, ``lag`` is padded with ``n`` zeros, which eliminates the assumption of repetition. axis : int The axis to keep as the ``time`` axis. The alternate axis will be converted to lag coordinates. Returns ------- lag : np.ndarray The recurrence matrix in (lag, time) (if ``axis=1``) or (time, lag) (if ``axis=0``) coordinates Raises ------ ParameterError : if ``rec`` is non-square See Also -------- recurrence_matrix lag_to_recurrence util.shear Examples -------- >>> y, sr = librosa.loadx('nutcracker') >>> hop_length = 1024 >>> chroma = librosa.feature.chroma_cqt(y=y, sr=sr, hop_length=hop_length) >>> chroma_stack = librosa.feature.stack_memory(chroma, n_steps=10, delay=3) >>> recurrence = librosa.segment.recurrence_matrix(chroma_stack) >>> lag_pad = librosa.segment.recurrence_to_lag(recurrence, pad=True) >>> lag_nopad = librosa.segment.recurrence_to_lag(recurrence, pad=False) >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=2, sharex=True) >>> librosa.display.specshow(lag_pad, x_axis='time', y_axis='lag', ... hop_length=hop_length, ax=ax[0]) >>> ax[0].set(title='Lag (zero-padded)') >>> ax[0].label_outer() >>> librosa.display.specshow(lag_nopad, x_axis='time', y_axis='lag', ... hop_length=hop_length, ax=ax[1]) >>> ax[1].set(title='Lag (no padding)') """ axis = np.abs(axis) if rec.ndim != 2 or rec.shape[0] != rec.shape[1]: raise ParameterError(f"non-square recurrence matrix shape: {rec.shape}") sparse = scipy.sparse.issparse(rec) if sparse: # suppress type check here, mypy doesn't know about issparse fmt = rec.format # type: ignore t = rec.shape[axis] if pad: if sparse: padding = np.asarray([[1, 0]], dtype=rec.dtype).swapaxes(axis, 0) rec_fmt: Literal["csr", "csc"] if axis == 0: rec_fmt = "csr" else: rec_fmt = "csc" rec = scipy.sparse.kron(padding, rec, format=rec_fmt) # type: ignore else: padding = np.array([(0, 0), (0, 0)]) padding[(1 - axis), :] = [0, t] # Suppress type check, mypy doesn't know that rec is an ndarray here rec = np.pad(rec, padding, mode="constant") # type: ignore lag: _ArrayOrSparseArray = util.shear(rec, factor=-1, axis=axis) # type: ignore[arg-type, assignment] if sparse: # Suppress type check, mypy doesn't know # that lag is sparse here lag = lag.asformat(fmt) # type: ignore return lag def lag_to_recurrence( lag: _ArrayOrSparseArray, *, axis: int = -1 ) -> _ArrayOrSparseArray: """Convert a lag matrix into a recurrence matrix. Parameters ---------- lag : np.ndarray or scipy.sparse array A lag matrix, as produced by ``recurrence_to_lag`` axis : int The axis corresponding to the time dimension. The alternate axis will be interpreted in lag coordinates. Returns ------- rec : np.ndarray or scipy.sparse array [shape=(n, n)] A recurrence matrix in (time, time) coordinates For sparse arrays, format will match that of ``lag``. Raises ------ ParameterError : if ``lag`` does not have the correct shape See Also -------- recurrence_to_lag Examples -------- >>> y, sr = librosa.loadx('nutcracker') >>> hop_length = 1024 >>> chroma = librosa.feature.chroma_cqt(y=y, sr=sr, hop_length=hop_length) >>> chroma_stack = librosa.feature.stack_memory(chroma, n_steps=10, delay=3) >>> recurrence = librosa.segment.recurrence_matrix(chroma_stack) >>> lag_pad = librosa.segment.recurrence_to_lag(recurrence, pad=True) >>> lag_nopad = librosa.segment.recurrence_to_lag(recurrence, pad=False) >>> rec_pad = librosa.segment.lag_to_recurrence(lag_pad) >>> rec_nopad = librosa.segment.lag_to_recurrence(lag_nopad) >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=2, ncols=2, sharex=True) >>> librosa.display.specshow(lag_pad, x_axis='s', y_axis='lag', ... hop_length=hop_length, ax=ax[0, 0]) >>> ax[0, 0].set(title='Lag (zero-padded)') >>> ax[0, 0].label_outer() >>> librosa.display.specshow(lag_nopad, x_axis='s', y_axis='time', ... hop_length=hop_length, ax=ax[0, 1]) >>> ax[0, 1].set(title='Lag (no padding)') >>> ax[0, 1].label_outer() >>> librosa.display.specshow(rec_pad, x_axis='s', y_axis='time', ... hop_length=hop_length, ax=ax[1, 0]) >>> ax[1, 0].set(title='Recurrence (with padding)') >>> librosa.display.specshow(rec_nopad, x_axis='s', y_axis='time', ... hop_length=hop_length, ax=ax[1, 1]) >>> ax[1, 1].set(title='Recurrence (without padding)') >>> ax[1, 1].label_outer() """ if axis not in [0, 1, -1]: raise ParameterError(f"Invalid target axis: {axis}") axis = np.abs(axis) if lag.ndim != 2 or ( lag.shape[0] != lag.shape[1] and lag.shape[1 - axis] != 2 * lag.shape[axis] ): raise ParameterError(f"Invalid lag matrix shape: {lag.shape}") # Since lag must be 2-dimensional, abs(axis) = axis t = lag.shape[axis] rec = util.shear(lag, factor=+1, axis=axis) # type: ignore[arg-type] sub_slice = [slice(None)] * rec.ndim sub_slice[1 - axis] = slice(t) rec_slice: _ArrayOrSparseArray = rec[tuple(sub_slice)] # type: ignore[assignment] return rec_slice def timelag_filter[F: Callable[..., Any]](function: F, pad: bool = True, index: int = 0) -> F: """Apply a filter in the time-lag domain. This is primarily useful for adapting image filters to operate on `recurrence_to_lag` output. Using `timelag_filter` is equivalent to the following sequence of operations: >>> data_tl = librosa.segment.recurrence_to_lag(data) >>> data_filtered_tl = function(data_tl) >>> data_filtered = librosa.segment.lag_to_recurrence(data_filtered_tl) Parameters ---------- function : callable The filtering function to wrap, e.g., `scipy.ndimage.median_filter` pad : bool Whether to zero-pad the structure feature matrix index : int >= 0 If ``function`` accepts input data as a positional argument, it should be indexed by ``index`` Returns ------- wrapped_function : callable A new filter function which applies in time-lag space rather than time-time space. Examples -------- Apply a 31-bin median filter to the diagonal of a recurrence matrix. With default, parameters, this corresponds to a time window of about 0.72 seconds. >>> y, sr = librosa.loadx('nutcracker', duration=30) >>> chroma = librosa.feature.chroma_cqt(y=y, sr=sr) >>> chroma_stack = librosa.feature.stack_memory(chroma, n_steps=3, delay=3) >>> rec = librosa.segment.recurrence_matrix(chroma_stack) >>> from scipy.ndimage import median_filter >>> diagonal_median = librosa.segment.timelag_filter(median_filter) >>> rec_filtered = diagonal_median(rec, size=(1, 31), mode='mirror') Or with affinity weights >>> rec_aff = librosa.segment.recurrence_matrix(chroma_stack, mode='affinity') >>> rec_aff_fil = diagonal_median(rec_aff, size=(1, 31), mode='mirror') >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=2, ncols=2, sharex=True, sharey=True) >>> librosa.display.specshow(rec, y_axis='s', x_axis='s', ax=ax[0, 0]) >>> ax[0, 0].set(title='Raw recurrence matrix') >>> ax[0, 0].label_outer() >>> librosa.display.specshow(rec_filtered, y_axis='s', x_axis='s', ax=ax[0, 1]) >>> ax[0, 1].set(title='Filtered recurrence matrix') >>> ax[0, 1].label_outer() >>> librosa.display.specshow(rec_aff, x_axis='s', y_axis='s', ... cmap='magma_r', ax=ax[1, 0]) >>> ax[1, 0].set(title='Raw affinity matrix') >>> librosa.display.specshow(rec_aff_fil, x_axis='s', y_axis='s', ... cmap='magma_r', ax=ax[1, 1]) >>> ax[1, 1].set(title='Filtered affinity matrix') >>> ax[1, 1].label_outer() """ def __my_filter(wrapped_f, *args, **kwargs): """Wrap the filter with lag conversions""" # Map the input data into time-lag space args = list(args) args[index] = recurrence_to_lag(args[index], pad=pad) # Apply the filtering function result = wrapped_f(*args, **kwargs) # Map back into time-time and return return lag_to_recurrence(result) return decorator(__my_filter, function) # type: ignore @cache(level=30) def subsegment( data: np.ndarray, frames: np.ndarray, *, n_segments: int = 4, axis: int = -1 ) -> _Array1D[np.int_]: """Sub-divide a segmentation by feature clustering. Given a set of frame boundaries (``frames``), and a data matrix (``data``), each successive interval defined by ``frames`` is partitioned into ``n_segments`` by constrained agglomerative clustering. .. note:: If an interval spans fewer than ``n_segments`` frames, then each frame becomes a sub-segment. Parameters ---------- data : np.ndarray Data matrix to use in clustering frames : np.ndarray [shape=(n_boundaries,)], dtype=int, non-negative] Array of beat or segment boundaries, as provided by `librosa.beat.beat_track`, `librosa.onset.onset_detect`, or `agglomerative`. n_segments : int > 0 Maximum number of frames to sub-divide each interval. axis : int Axis along which to apply the segmentation. By default, the last index (-1) is taken. Returns ------- boundaries : np.ndarray [shape=(n_subboundaries,)] List of sub-divided segment boundaries See Also -------- agglomerative : Temporal segmentation librosa.onset.onset_detect : Onset detection librosa.beat.beat_track : Beat tracking Notes ----- This function caches at level 30. Examples -------- Load audio, detect beat frames, and subdivide in twos by CQT >>> y, sr = librosa.loadx('choice', duration=6.5) >>> tempo, beats = librosa.beat.beat_track(y=y, sr=sr, hop_length=512) >>> beat_times = librosa.frames_to_time(beats, sr=sr, hop_length=512) >>> cqt = np.abs(librosa.cqt(y, sr=sr, bins_per_octave=36, n_bins=36*7, hop_length=512)) >>> subseg = librosa.segment.subsegment(cqt, beats, n_segments=2) >>> subseg_t = librosa.frames_to_time(subseg, sr=sr, hop_length=512) >>> import matplotlib.pyplot as plt >>> import matplotlib.transforms as mpt >>> fig, ax = plt.subplots() >>> librosa.display.specshow(cqt, vscale='dBFS', bins_per_octave=36, ... y_axis='cqt_hz', x_axis='time', ax=ax) >>> hl = librosa.display.highlight(ax=ax, alpha=0.75, linewidth=2) >>> trans = mpt.blended_transform_factory( ... ax.transData, ax.transAxes) >>> ax.plot(beat_times, np.zeros_like(beat_times), '^', zorder=4, ... markerfacecolor='C0', color='C0', linestyle='', clip_on=False, ... markersize=10, label='Beats', transform=trans, path_effects=hl) >>> ax.vlines(beat_times, 0, 1, color='C0', linestyle='-', transform=trans, ... linewidth=3, alpha=0.9, zorder=1.5, path_effects=hl) >>> ax.plot(subseg_t, np.zeros_like(subseg_t), '^', zorder=3, ... markerfacecolor='C2', color='C2', linestyle='', clip_on=False, ... markersize=6, label='Sub-beats', transform=trans, path_effects=hl) >>> ax.vlines(subseg_t, 0, 1, color='C2', linestyle='--', transform=trans, ... linewidth=1, alpha=0.9, path_effects=hl) >>> ax.legend(loc='upper right') >>> ax.set(title='CQT + Beat and sub-beat markers') """ frames = util.fix_frames(frames, x_min=0, x_max=data.shape[axis], pad=True) if n_segments < 1: raise ParameterError("n_segments must be a positive integer") boundaries: list = [] idx_slices = [slice(None)] * data.ndim for seg_start, seg_end in itertools.pairwise(frames): idx_slices[axis] = slice(seg_start, seg_end) boundaries.extend( seg_start + agglomerative( data[tuple(idx_slices)], cast("np.integer", min(seg_end - seg_start, n_segments)), axis=axis, ) ) return np.array(boundaries) def agglomerative( data: np.ndarray, k: int | np.integer, *, clusterer: sklearn.cluster.AgglomerativeClustering | None = None, axis: int = -1, ) -> _Array1D[np.int_]: """Bottom-up temporal segmentation. Use a temporally-constrained agglomerative clustering routine to partition ``data`` into ``k`` contiguous segments. Parameters ---------- data : np.ndarray data to cluster k : int > 0 [scalar] number of segments to produce clusterer : sklearn.cluster.AgglomerativeClustering, optional An optional AgglomerativeClustering object. If `None`, a constrained Ward object is instantiated. axis : int axis along which to cluster. By default, the last axis (-1) is chosen. Returns ------- boundaries : np.ndarray [shape=(k,)] left-boundaries (frame numbers) of detected segments. This will always include `0` as the first left-boundary. See Also -------- sklearn.cluster.AgglomerativeClustering Examples -------- Cluster by chroma similarity, break into 20 segments >>> y, sr = librosa.loadx('nutcracker', duration=15) >>> chroma = librosa.feature.chroma_cqt(y=y, sr=sr, threshold=0.5) >>> bounds = librosa.segment.agglomerative(chroma, 20) >>> bound_times = librosa.frames_to_time(bounds, sr=sr) >>> bound_times array([ 0. , 0.65 , 1.091, 1.927, 2.438, 2.902, 3.924, 4.783, 5.294, 5.712, 6.13 , 7.314, 8.522, 8.916, 9.66 , 10.844, 11.238, 12.028, 12.492, 14.095]) Plot the segmentation over the chromagram >>> import matplotlib.pyplot as plt >>> import matplotlib.transforms as mpt >>> fig, ax = plt.subplots() >>> trans = mpt.blended_transform_factory( ... ax.transData, ax.transAxes) >>> librosa.display.specshow(chroma, y_axis='chroma', x_axis='time', ax=ax) >>> hl = librosa.display.highlight(ax=ax) >>> ax.vlines(bound_times, 0, 1, linestyle='--', path_effects=hl, ... label='Segment boundaries', ... transform=trans) >>> ax.legend() >>> ax.set(title='Power spectrogram') """ # Make sure we have at least two dimensions data = np.atleast_2d(data) # Swap data index to position 0 data = np.swapaxes(data, axis, 0) # Flatten the features n = data.shape[0] data = data.reshape((n, -1), order="F") if clusterer is None: import sklearn.cluster import sklearn.feature_extraction # Connect the temporal connectivity graph grid = sklearn.feature_extraction.image.grid_to_graph(n_x=n, n_y=1, n_z=1) # Instantiate the clustering object clusterer = sklearn.cluster.AgglomerativeClustering( n_clusters=k, connectivity=grid, memory=cache.memory ) # Fit the model clusterer.fit(data) # Find the change points from the labels boundaries = [0] boundaries.extend(list(1 + np.nonzero(np.diff(clusterer.labels_))[0].astype(int))) return np.asarray(boundaries) def path_enhance( R: np.ndarray, n: int, *, window: _WindowSpec = "hann", max_ratio: float = 2.0, min_ratio: float | None = None, n_filters: int = 7, zero_mean: bool = False, clip: bool = True, **kwargs: Any, ) -> np.ndarray: """Multi-angle path enhancement for self- and cross-similarity matrices. This function convolves multiple diagonal smoothing filters with a self-similarity (or recurrence) matrix R, and aggregates the result by an element-wise maximum. Technically, the output is a matrix R_smooth such that:: R_smooth[i, j] = max_theta (R * filter_theta)[i, j] where `*` denotes 2-dimensional convolution, and ``filter_theta`` is a smoothing filter at orientation theta. This is intended to provide coherent temporal smoothing of self-similarity matrices when there are changes in tempo. Smoothing filters are generated at evenly spaced orientations between min_ratio and max_ratio. This function is inspired by the multi-angle path enhancement of [#]_, but differs by modeling tempo differences in the space of similarity matrices rather than re-sampling the underlying features prior to generating the self-similarity matrix. .. [#] Müller, Meinard and Frank Kurth. "Enhancing similarity matrices for music audio analysis." 2006 IEEE International Conference on Acoustics Speech and Signal Processing Proceedings. Vol. 5. IEEE, 2006. .. note:: if using recurrence_matrix to construct the input similarity matrix, be sure to include the main diagonal by setting ``self=True``. Otherwise, the diagonal will be suppressed, and this is likely to produce discontinuities which will pollute the smoothing filter response. Parameters ---------- R : np.ndarray The self- or cross-similarity matrix to be smoothed. Note: sparse inputs are not supported. If the recurrence matrix is multi-dimensional, e.g. `shape=(c, n, n)`, then enhancement is conducted independently for each leading channel. n : int > 0 The length of the smoothing filter window : window specification The type of smoothing filter to use. See `filters.get_window` for more information on window specification formats. max_ratio : float > 0 The maximum tempo ratio to support min_ratio : float > 0 The minimum tempo ratio to support. If not provided, it will default to ``1/max_ratio`` n_filters : int >= 1 The number of different smoothing filters to use, evenly spaced between ``min_ratio`` and ``max_ratio``. If ``min_ratio = 1/max_ratio`` (the default), using an odd number of filters will ensure that the main diagonal (ratio=1) is included. zero_mean : bool By default, the smoothing filters are non-negative and sum to one (i.e. are averaging filters). If ``zero_mean=True``, then the smoothing filters are made to sum to zero by subtracting a constant value from the non-diagonal coordinates of the filter. This is primarily useful for suppressing blocks while enhancing diagonals. clip : bool If True, the smoothed similarity matrix will be thresholded at 0, and will not contain negative entries. **kwargs : additional keyword arguments Additional arguments to pass to `scipy.ndimage.convolve` Returns ------- R_smooth : np.ndarray, shape=R.shape The smoothed self- or cross-similarity matrix See Also -------- librosa.filters.diagonal_filter recurrence_matrix Examples -------- Use a 51-frame diagonal smoothing filter to enhance paths in a recurrence matrix >>> y, sr = librosa.loadx('nutcracker') >>> hop_length = 2048 >>> chroma = librosa.feature.chroma_cqt(y=y, sr=sr, hop_length=hop_length) >>> chroma_stack = librosa.feature.stack_memory(chroma, n_steps=10, delay=3) >>> rec = librosa.segment.recurrence_matrix(chroma_stack, mode='affinity', self=True) >>> rec_smooth = librosa.segment.path_enhance(rec, 51, window='hann', n_filters=7) Plot the recurrence matrix before and after smoothing >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(ncols=2, sharex=True, sharey=True) >>> img = librosa.display.specshow(rec, x_axis='s', y_axis='s', ... hop_length=hop_length, ax=ax[0]) >>> ax[0].set(title='Unfiltered recurrence') >>> imgpe = librosa.display.specshow(rec_smooth, x_axis='s', y_axis='s', ... hop_length=hop_length, ax=ax[1]) >>> ax[1].set(title='Multi-angle enhanced recurrence') >>> ax[1].label_outer() >>> fig.colorbar(img, ax=ax[0], orientation='horizontal') >>> fig.colorbar(imgpe, ax=ax[1], orientation='horizontal') """ if min_ratio is None: min_ratio = 1.0 / max_ratio elif min_ratio > max_ratio: raise ParameterError( f"min_ratio={min_ratio} cannot exceed max_ratio={max_ratio}" ) R_smooth = None for ratio in np.logspace( np.log2(min_ratio), np.log2(max_ratio), num=n_filters, base=2 ): kernel = diagonal_filter(window, n, slope=ratio, zero_mean=zero_mean) # Expand leading dimensions to match R # This way, if R has shape, eg, [2, 3, n, n] # the expanded kernel will have shape [1, 1, m, m] # The following is valid for numpy >= 1.18 # kernel = np.expand_dims(kernel, axis=list(np.arange(R.ndim - kernel.ndim))) # This is functionally equivalent, but works on numpy 1.17 shape = [1] * R.ndim shape[-2:] = kernel.shape kernel = np.reshape(kernel, shape) import scipy.ndimage if R_smooth is None: R_smooth = scipy.ndimage.convolve(R, kernel, **kwargs) else: # Compute the point-wise maximum in-place np.maximum( R_smooth, scipy.ndimage.convolve(R, kernel, **kwargs), out=R_smooth ) if clip: # Clip the output in-place np.clip(R_smooth, 0, None, out=R_smooth) # type: ignore return np.asanyarray(R_smooth) def __affinity_bandwidth( rec: scipy.sparse.csr_array, bw_mode: np.ndarray | _FloatLike_co | str | None, k: int, ) -> float | np.ndarray: # rec should be a csr_array # the api allows users to specify a scalar bandwidth directly, besides the string based options. if isinstance(bw_mode, np.ndarray): bandwidth = bw_mode # check if bw is the right size if bandwidth.shape != rec.shape: raise ParameterError( f"Invalid matrix bandwidth shape: {bandwidth.shape}." f"Should be {rec.shape}." ) if (bandwidth <= 0).any(): raise ParameterError( "Invalid bandwidth. All entries must be strictly positive." ) return np.array(bandwidth[rec.nonzero()]) elif isinstance(bw_mode, (int, float)): scalar_bandwidth = float(bw_mode) if scalar_bandwidth <= 0: raise ParameterError( f"Invalid scalar bandwidth={scalar_bandwidth}. Must be strictly positive." ) return scalar_bandwidth if bw_mode is None: bw_mode = "med_k_scalar" if bw_mode not in [ "med_k_scalar", "mean_k", "gmean_k", "mean_k_avg", "gmean_k_avg", "mean_k_avg_and_pair", ]: raise ParameterError( f"Invalid bandwidth='{bw_mode}'. Must be either a positive scalar or one of " "['med_k_scalar', 'mean_k', 'gmean_k', 'mean_k_avg', 'gmean_k_avg', 'mean_k_avg_and_pair']" ) # build a list of list that stores the distances to k nearest neighbors for all t points. t = rec.shape[0] knn_dists = [] for i in range(t): # Get the links from point i using CSR format start, end = rec.indptr[i], rec.indptr[i + 1] row_data = rec.data[start:end] # catch empty dists lists in knn_dists if row_data.size == 0: # Disconnected vertices are only a problem for point-wise bandwidth estimation if bw_mode not in ["med_k_scalar"]: raise ParameterError(f"The sample at time point {i} has no neighbors") knn_dists.append(np.array([np.nan])) else: knn_dists.append(np.sort(row_data)[:k]) # take the last element of each list for the distance to kth neighbor dist_to_k = np.asarray([dists[-1] for dists in knn_dists]) avg_dist_to_first_ks = np.asarray([np.mean(dists) for dists in knn_dists]) if bw_mode == "med_k_scalar": if not np.any(np.isfinite(dist_to_k)): raise ParameterError("Cannot estimate bandwidth from an empty graph") return float(np.nanmedian(dist_to_k)) if bw_mode in ["mean_k", "gmean_k"]: # building bandwidth components (sigma) using sparse matrix structures and indices sigma_i_data = np.empty_like(rec.data) sigma_j_data = np.empty_like(rec.data) for row in range(t): sigma_i_data[rec.indptr[row] : rec.indptr[row + 1]] = dist_to_k[row] col_idx = rec.indices[rec.indptr[row] : rec.indptr[row + 1]] sigma_j_data[rec.indptr[row] : rec.indptr[row + 1]] = dist_to_k[col_idx] if bw_mode == "mean_k": out = np.array((sigma_i_data + sigma_j_data) / 2) elif bw_mode == "gmean_k": out = np.array((sigma_i_data * sigma_j_data) ** 0.5) if bw_mode in ["mean_k_avg", "gmean_k_avg", "mean_k_avg_and_pair"]: # building bandwidth components (sigma) using sparse matrix structures and indices sigma_i_data = np.empty_like(rec.data) sigma_j_data = np.empty_like(rec.data) for row in range(t): sigma_i_data[rec.indptr[row] : rec.indptr[row + 1]] = avg_dist_to_first_ks[ row ] col_idx = rec.indices[rec.indptr[row] : rec.indptr[row + 1]] sigma_j_data[rec.indptr[row] : rec.indptr[row + 1]] = avg_dist_to_first_ks[ col_idx ] if bw_mode == "mean_k_avg": out = np.array((sigma_i_data + sigma_j_data) / 2) elif bw_mode == "gmean_k_avg": out = np.array((sigma_i_data * sigma_j_data) ** 0.5) elif bw_mode == "mean_k_avg_and_pair": out = np.array((sigma_i_data + sigma_j_data + rec.data) / 3) return out ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2125447 librosa-1.0.0/librosa/sequence.py0000644000175100017510000022376115236640653016453 0ustar00runnerrunner#!/usr/bin/env python # -*- encoding: utf-8 -*- """ librosa.sequence provides functions for sequential processing, such as dynamic time warping (DTW), recurrence quantification analysis (RQA), and various forms of Viterbi decoding. Helper utilities are also provided for constructing transition matrices or post-processing alignment paths. """ from __future__ import annotations from typing import TYPE_CHECKING, cast, overload import numpy as np from numba import jit from .filters import get_window from .util import expand_to, fill_off_diagonal, is_positive_int, pad_center, tiny from .util.exceptions import ParameterError if TYPE_CHECKING: from typing import Any, Iterable, Literal import numpy.typing as npt from ._typing import _Array2D, _WindowSpec __all__ = [ "dtw", "dtw_backtracking", "path_to_steps", "rqa", "viterbi", "viterbi_discriminative", "viterbi_binary", "transition_uniform", "transition_loop", "transition_cycle", "transition_local", ] @overload def dtw( X: np.ndarray, Y: np.ndarray, *, metric: str = ..., step_sizes_sigma: np.ndarray | None = ..., weights_add: np.ndarray | None = ..., weights_mul: np.ndarray | None = ..., subseq: bool = ..., backtrack: Literal[False], global_constraints: bool = ..., band_rad: float = ..., return_steps: Literal[False] = ..., ) -> _Array2D[np.float64]: ... @overload def dtw( *, C: np.ndarray, metric: str = ..., step_sizes_sigma: np.ndarray | None = ..., weights_add: np.ndarray | None = ..., weights_mul: np.ndarray | None = ..., subseq: bool = ..., backtrack: Literal[False], global_constraints: bool = ..., band_rad: float = ..., return_steps: Literal[False] = ..., ) -> _Array2D[np.float64]: ... @overload def dtw( X: np.ndarray, Y: np.ndarray, *, metric: str = ..., step_sizes_sigma: np.ndarray | None = ..., weights_add: np.ndarray | None = ..., weights_mul: np.ndarray | None = ..., subseq: bool = ..., backtrack: Literal[False], global_constraints: bool = ..., band_rad: float = ..., return_steps: Literal[True], ) -> tuple[_Array2D[np.float64], _Array2D[np.int32]]: ... @overload def dtw( *, C: np.ndarray, metric: str = ..., step_sizes_sigma: np.ndarray | None = ..., weights_add: np.ndarray | None = ..., weights_mul: np.ndarray | None = ..., subseq: bool = ..., backtrack: Literal[False], global_constraints: bool = ..., band_rad: float = ..., return_steps: Literal[True], ) -> tuple[_Array2D[np.float64], _Array2D[np.int32]]: ... @overload def dtw( X: np.ndarray, Y: np.ndarray, *, metric: str = ..., step_sizes_sigma: np.ndarray | None = ..., weights_add: np.ndarray | None = ..., weights_mul: np.ndarray | None = ..., subseq: bool = ..., backtrack: Literal[True] = ..., global_constraints: bool = ..., band_rad: float = ..., return_steps: Literal[False] = ..., ) -> tuple[_Array2D[np.float64], _Array2D[np.int_]]: ... @overload def dtw( *, C: np.ndarray, metric: str = ..., step_sizes_sigma: np.ndarray | None = ..., weights_add: np.ndarray | None = ..., weights_mul: np.ndarray | None = ..., subseq: bool = ..., backtrack: Literal[True] = ..., global_constraints: bool = ..., band_rad: float = ..., return_steps: Literal[False] = ..., ) -> tuple[_Array2D[np.float64], _Array2D[np.int_]]: ... @overload def dtw( X: np.ndarray, Y: np.ndarray, *, metric: str = ..., step_sizes_sigma: np.ndarray | None = ..., weights_add: np.ndarray | None = ..., weights_mul: np.ndarray | None = ..., subseq: bool = ..., backtrack: Literal[True] = ..., global_constraints: bool = ..., band_rad: float = ..., return_steps: Literal[True], ) -> tuple[_Array2D[np.float64], _Array2D[np.int_], _Array2D[np.int32]]: ... @overload def dtw( *, C: np.ndarray, metric: str = ..., step_sizes_sigma: np.ndarray | None = ..., weights_add: np.ndarray | None = ..., weights_mul: np.ndarray | None = ..., subseq: bool = ..., backtrack: Literal[True] = ..., global_constraints: bool = ..., band_rad: float = ..., return_steps: Literal[True], ) -> tuple[_Array2D[np.float64], _Array2D[np.int_], _Array2D[np.int32]]: ... def dtw( X: np.ndarray | None = None, Y: np.ndarray | None = None, *, C: np.ndarray | None = None, metric: str = "euclidean", step_sizes_sigma: np.ndarray | None = None, weights_add: np.ndarray | None = None, weights_mul: np.ndarray | None = None, subseq: bool = False, backtrack: bool = True, global_constraints: bool = False, band_rad: float = 0.25, return_steps: bool = False, ) -> ( _Array2D[np.float64] | tuple[_Array2D[np.float64], _Array2D[np.int_] | _Array2D[np.int32]] | tuple[_Array2D[np.float64], _Array2D[np.int_], _Array2D[np.int32]] ): """Dynamic time warping (DTW). This function performs a DTW and path backtracking on two sequences. We follow the nomenclature and algorithmic approach as described in [#]_. .. [#] Meinard Mueller Fundamentals of Music Processing — Audio, Analysis, Algorithms, Applications Springer Verlag, ISBN: 978-3-319-21944-8, 2015. Parameters ---------- X : np.ndarray [shape=(..., K, N)] audio feature matrix (e.g., chroma features) If ``X`` has more than two dimensions (e.g., for multi-channel inputs), all leading dimensions are used when computing distance to ``Y``. Y : np.ndarray [shape=(..., K, M)] audio feature matrix (e.g., chroma features) C : np.ndarray [shape=(N, M)] Precomputed distance matrix. If supplied, X and Y must not be supplied and ``metric`` will be ignored. metric : str Identifier for the cost-function as documented in `scipy.spatial.distance.cdist()` step_sizes_sigma : np.ndarray [shape=[n, 2]] Specifies allowed step sizes as used by the DTW. weights_add : np.ndarray [shape=[n, ]] Additive weights to penalize certain step sizes. weights_mul : np.ndarray [shape=[n, ]] Multiplicative weights to penalize certain step sizes. subseq : bool Enable subsequence DTW, e.g., for retrieval tasks. backtrack : bool Enable backtracking in accumulated cost matrix. global_constraints : bool Applies global constraints to the cost matrix ``C`` (Sakoe-Chiba band). band_rad : float The Sakoe-Chiba band radius (1/2 of the width) will be ``int(radius*min(C.shape))``. return_steps : bool If true, the function returns ``steps``, the step matrix, containing the indices of the used steps from the cost accumulation step. Returns ------- D : np.ndarray [shape=(N, M)] accumulated cost matrix. The value at the final index position ``D[-1, -1]`` is the total alignment cost. wp : np.ndarray [shape=(L, 2)] Warping path with index pairs. Each row of the array contains an index pair (n, m). Only returned when ``backtrack`` is True. Note that the length ``L`` of the warping path need not match the lengths of the input data, depending on the ``step_sizes_sigma`` values and ``subseq``. steps : np.ndarray [shape=(N, M)] Step matrix, containing the indices of the used steps from the cost accumulation step. Only returned when ``return_steps`` is True. Raises ------ ParameterError If you are doing diagonal matching and Y is shorter than X or if an incompatible combination of X, Y, and C are supplied. If your input dimensions are incompatible. If the cost matrix has NaN values. Examples -------- >>> import numpy as np >>> import matplotlib.pyplot as plt >>> y, sr = librosa.loadx('brahms', offset=10, duration=15) >>> X = librosa.feature.chroma_cens(y=y, sr=sr) >>> noise = np.random.rand(X.shape[0], 200) >>> Y = np.concatenate((noise, noise, X, noise), axis=1) >>> D, wp = librosa.sequence.dtw(X, Y, subseq=True) >>> fig, ax = plt.subplots(nrows=2, sharex=True) >>> img = librosa.display.specshow(D, x_axis='frames', y_axis='frames', ... ax=ax[0]) >>> hl = librosa.display.highlight(ax=ax[0]) >>> ax[0].set(title='DTW cost', xlabel='Noisy sequence', ylabel='Target') >>> ax[0].plot(wp[:, 1], wp[:, 0], label='Optimal path', path_effects=hl) >>> ax[0].legend() >>> fig.colorbar(img, ax=ax[0]) >>> ax[1].plot(D[-1, :] / wp.shape[0]) >>> ax[1].set(xlim=[0, Y.shape[1]], ylim=[0, 2], ... title='Matching cost function') """ # Default Parameters default_steps = np.array([[1, 1], [0, 1], [1, 0]], dtype=np.uint32) default_weights_add = np.zeros(3, dtype=np.float64) default_weights_mul = np.ones(3, dtype=np.float64) if step_sizes_sigma is None: # Use the default steps step_sizes_sigma = default_steps # Use default weights if none are provided if weights_add is None: weights_add = default_weights_add if weights_mul is None: weights_mul = default_weights_mul else: # If we have custom steps but no weights, construct them here if weights_add is None: weights_add = np.zeros(len(step_sizes_sigma), dtype=np.float64) if weights_mul is None: weights_mul = np.ones(len(step_sizes_sigma), dtype=np.float64) # Make the default step weights infinite so that they are never # preferred over custom steps default_weights_add.fill(np.inf) default_weights_mul.fill(np.inf) # Append custom steps and weights to our defaults step_sizes_sigma = np.concatenate((default_steps, step_sizes_sigma)) weights_add = np.concatenate((default_weights_add, weights_add)) weights_mul = np.concatenate((default_weights_mul, weights_mul)) # These asserts are bad, but mypy cannot trace the code paths properly assert step_sizes_sigma is not None assert weights_add is not None assert weights_mul is not None if np.any(step_sizes_sigma < 0): raise ParameterError("step_sizes_sigma cannot contain negative values") if len(step_sizes_sigma) != len(weights_add): raise ParameterError("len(weights_add) must be equal to len(step_sizes_sigma)") if len(step_sizes_sigma) != len(weights_mul): raise ParameterError("len(weights_mul) must be equal to len(step_sizes_sigma)") if C is None and (X is None or Y is None): raise ParameterError("If C is not supplied, both X and Y must be supplied") if C is not None and (X is not None or Y is not None): raise ParameterError("If C is supplied, both X and Y must not be supplied") c_is_transposed = False # calculate pair-wise distances, unless already supplied. # C_local will keep track of whether the distance matrix was supplied # by the user (False) or constructed locally (True) C_local = False if C is None: C_local = True # mypy can't figure out that this case does not happen assert X is not None and Y is not None # take care of dimensions X = np.atleast_2d(X) Y = np.atleast_2d(Y) # Perform some shape-squashing here # Put the time axes around front # Suppress types because mypy doesn't know these are ndarrays X = np.swapaxes(X, -1, 0) Y = np.swapaxes(Y, -1, 0) # Flatten the remaining dimensions # Use F-ordering to preserve columns X = X.reshape((X.shape[0], -1), order="F") Y = Y.reshape((Y.shape[0], -1), order="F") try: from scipy.spatial.distance import cdist C = cdist(X, Y, metric=metric) # type: ignore[call-overload] except ValueError as exc: raise ParameterError( "scipy.spatial.distance.cdist returned an error.\n" "Please provide your input in the form X.shape=(K, N) " "and Y.shape=(K, M).\n 1-dimensional sequences should " "be reshaped to X.shape=(1, N) and Y.shape=(1, M)." ) from exc # for subsequence matching: # if N > M, Y can be a subsequence of X if subseq and (X.shape[0] > Y.shape[0]): C = C.T c_is_transposed = True C = np.atleast_2d(C) # if diagonal matching, Y has to be longer than X # (X simply cannot be contained in Y) if np.array_equal(step_sizes_sigma, np.array([[1, 1]])) and ( C.shape[0] > C.shape[1] ): raise ParameterError( "For diagonal matching: Y.shape[-1] >= X.shape[-11] " "(C.shape[1] >= C.shape[0])" ) max_0 = step_sizes_sigma[:, 0].max() max_1 = step_sizes_sigma[:, 1].max() # check C here for nans before building global constraints if np.any(np.isnan(C)): raise ParameterError("DTW cost matrix C has NaN values. ") if global_constraints: # Apply global constraints to the cost matrix if not C_local: # If C was provided as input, make a copy here C = np.copy(C) fill_off_diagonal(C, radius=band_rad, value=np.inf) # initialize whole matrix with infinity values D = np.ones(C.shape + np.array([max_0, max_1])) * np.inf # set starting point to C[0, 0] D[max_0, max_1] = C[0, 0] if subseq: D[max_0, max_1:] = C[0, :] # initialize step matrix with -1 # will be filled in calc_accu_cost() with indices from step_sizes_sigma steps = np.zeros(D.shape, dtype=np.int32) # these steps correspond to left- (first row) and up-(first column) moves steps[0, :] = 1 steps[:, 0] = 2 # calculate accumulated cost matrix D, steps = __dtw_calc_accu_cost( C, D, steps, step_sizes_sigma, weights_mul, weights_add, max_0, max_1 ) # delete infinity rows and columns D = D[max_0:, max_1:] steps = steps[max_0:, max_1:] return_values: list[np.ndarray] if backtrack: wp: np.ndarray if subseq: if np.all(np.isinf(D[-1])): raise ParameterError( "No valid sub-sequence warping path could " "be constructed with the given step sizes." ) start = np.argmin(D[-1, :]) _wp = __dtw_backtracking(steps, step_sizes_sigma, subseq, start) else: # perform warping path backtracking if np.isinf(D[-1, -1]): raise ParameterError( "No valid sub-sequence warping path could " "be constructed with the given step sizes." ) _wp = __dtw_backtracking(steps, step_sizes_sigma, subseq) if _wp[-1] != (0, 0): raise ParameterError( "Unable to compute a full DTW warping path. " "You may want to try again with subseq=True." ) wp = np.asarray(_wp, dtype=int) # since we transposed in the beginning, we have to adjust the index pairs back if subseq and ( (X is not None and Y is not None and X.shape[0] > Y.shape[0]) or c_is_transposed or C.shape[0] > C.shape[1] ): wp = np.fliplr(wp) return_values = [D, wp] else: return_values = [D] if return_steps: return_values.append(steps) if len(return_values) > 1: # Suppressing type check here because mypy can't # infer the exact length of the tuple return tuple(return_values) # type: ignore else: return return_values[0] @jit(nopython=True, cache=True) # type: ignore def __dtw_calc_accu_cost( C: np.ndarray, D: np.ndarray, steps: np.ndarray, step_sizes_sigma: np.ndarray, weights_mul: np.ndarray, weights_add: np.ndarray, max_0: int, max_1: int, ) -> tuple[np.ndarray, np.ndarray]: # pragma: no cover """Calculate the accumulated cost matrix D. Use dynamic programming to calculate the accumulated costs. Parameters ---------- C : np.ndarray [shape=(N, M)] pre-computed cost matrix D : np.ndarray [shape=(N, M)] accumulated cost matrix steps : np.ndarray [shape=(N, M)] Step matrix, containing the indices of the used steps from the cost accumulation step. step_sizes_sigma : np.ndarray [shape=[n, 2]] Specifies allowed step sizes as used by the dtw. weights_add : np.ndarray [shape=[n, ]] Additive weights to penalize certain step sizes. weights_mul : np.ndarray [shape=[n, ]] Multiplicative weights to penalize certain step sizes. max_0 : int maximum number of steps in step_sizes_sigma in dim 0. max_1 : int maximum number of steps in step_sizes_sigma in dim 1. Returns ------- D : np.ndarray [shape=(N, M)] accumulated cost matrix. D[N, M] is the total alignment cost. When doing subsequence DTW, D[N,:] indicates a matching function. steps : np.ndarray [shape=(N, M)] Step matrix, containing the indices of the used steps from the cost accumulation step. See Also -------- dtw """ for cur_n in range(max_0, D.shape[0]): for cur_m in range(max_1, D.shape[1]): # accumulate costs for cur_step_idx, cur_w_add, cur_w_mul in zip( # noqa: B905 range(step_sizes_sigma.shape[0]), weights_add, weights_mul ): cur_D = D[ cur_n - step_sizes_sigma[cur_step_idx, 0], cur_m - step_sizes_sigma[cur_step_idx, 1], ] cur_C = cur_w_mul * C[cur_n - max_0, cur_m - max_1] cur_C += cur_w_add cur_cost = cur_D + cur_C # check if cur_cost is smaller than the one stored in D if cur_cost < D[cur_n, cur_m]: D[cur_n, cur_m] = cur_cost # save step-index steps[cur_n, cur_m] = cur_step_idx return D, steps @jit(nopython=True, cache=True) # type: ignore def __dtw_backtracking( steps: np.ndarray, step_sizes_sigma: np.ndarray, subseq: bool, start: int | None = None, ) -> list[tuple[int, int]]: # pragma: no cover """Backtrack optimal warping path. Uses the saved step sizes from the cost accumulation step to backtrack the index pairs for an optimal warping path. Parameters ---------- steps : np.ndarray [shape=(N, M)] Step matrix, containing the indices of the used steps from the cost accumulation step. step_sizes_sigma : np.ndarray [shape=[n, 2]] Specifies allowed step sizes as used by the dtw. subseq : bool Enable subsequence DTW, e.g., for retrieval tasks. start : int Start column index for backtraing (only allowed for ``subseq=True``) Returns ------- wp : list [shape=(N,)] Warping path with index pairs. Each list entry contains an index pair (n, m) as a tuple See Also -------- dtw """ if start is None: cur_idx = (steps.shape[0] - 1, steps.shape[1] - 1) else: cur_idx = (steps.shape[0] - 1, start) wp = [] # Set starting point D(N, M) and append it to the path wp.append((cur_idx[0], cur_idx[1])) # Loop backwards. # Stop criteria: # Setting it to (0, 0) does not work for the subsequence dtw, # so we only ask to reach the first row of the matrix. while (subseq and cur_idx[0] > 0) or (not subseq and cur_idx != (0, 0)): cur_step_idx = steps[(cur_idx[0], cur_idx[1])] # save tuple with minimal acc. cost in path cur_idx = ( cur_idx[0] - step_sizes_sigma[cur_step_idx][0], cur_idx[1] - step_sizes_sigma[cur_step_idx][1], ) # If we run off the side of the cost matrix, break here if min(cur_idx) < 0: break # append to warping path wp.append((cur_idx[0], cur_idx[1])) return wp def dtw_backtracking( steps: np.ndarray, *, step_sizes_sigma: np.ndarray | None = None, subseq: bool = False, start: int | np.integer[Any] | None = None, ) -> _Array2D[np.int_]: """Backtrack a warping path. Uses the saved step sizes from the cost accumulation step to backtrack the index pairs for a warping path. Parameters ---------- steps : np.ndarray [shape=(N, M)] Step matrix, containing the indices of the used steps from the cost accumulation step. step_sizes_sigma : np.ndarray [shape=[n, 2]] Specifies allowed step sizes as used by the dtw. subseq : bool Enable subsequence DTW, e.g., for retrieval tasks. start : int Start column index for backtraing (only allowed for ``subseq=True``) Returns ------- wp : list [shape=(N,)] Warping path with index pairs. Each list entry contains an index pair (n, m) as a tuple See Also -------- dtw """ if subseq is False and start is not None: raise ParameterError( f"start is only allowed to be set if subseq is True (start={start}, subseq={subseq})" ) # Default Parameters default_steps = np.array([[1, 1], [0, 1], [1, 0]], dtype=np.uint32) if step_sizes_sigma is None: # Use the default steps step_sizes_sigma = default_steps else: # Append custom steps and weights to our defaults step_sizes_sigma = np.concatenate((default_steps, step_sizes_sigma)) wp = __dtw_backtracking(steps, step_sizes_sigma, subseq, start) return np.asarray(wp, dtype=int) @overload def rqa( sim: np.ndarray, *, gap_onset: float = ..., gap_extend: float = ..., knight_moves: bool = ..., backtrack: Literal[False], ) -> _Array2D[Any]: ... @overload def rqa( sim: np.ndarray, *, gap_onset: float = ..., gap_extend: float = ..., knight_moves: bool = ..., backtrack: Literal[True] = ..., ) -> tuple[_Array2D[Any], _Array2D[np.uint]]: ... def rqa( sim: np.ndarray, *, gap_onset: float = 1, gap_extend: float = 1, knight_moves: bool = True, backtrack: bool = True, ) -> _Array2D[Any] | tuple[_Array2D[Any], _Array2D[np.uint]]: """Recurrence quantification analysis (RQA) This function implements different forms of RQA as described by Serra, Serra, and Andrzejak (SSA). [#]_ These methods take as input a self- or cross-similarity matrix ``sim``, and calculate the value of path alignments by dynamic programming. Note that unlike dynamic time warping (`dtw`), alignment paths here are maximized, not minimized, so the input should measure similarity rather than distance. The simplest RQA method, denoted as `L` (SSA equation 3) and equivalent to the method described by Eckman, Kamphorst, and Ruelle [#]_, accumulates the length of diagonal paths with positive values in the input: - ``score[i, j] = score[i-1, j-1] + 1`` if ``sim[i, j] > 0`` - ``score[i, j] = 0`` otherwise. The second method, denoted as `S` (SSA equation 4), is similar to the first, but allows for "knight moves" (as in the chess piece) in addition to strict diagonal moves: - ``score[i, j] = max(score[i-1, j-1], score[i-2, j-1], score[i-1, j-2]) + 1`` if ``sim[i, j] > 0`` - ``score[i, j] = 0`` otherwise. The third method, denoted as `Q` (SSA equations 5 and 6) extends this by allowing gaps in the alignment that incur some cost, rather than a hard reset to 0 whenever ``sim[i, j] == 0``. Gaps are penalized by two additional parameters, ``gap_onset`` and ``gap_extend``, which are subtracted from the value of the alignment path every time a gap is introduced or extended (respectively). Note that setting ``gap_onset`` and ``gap_extend`` to `np.inf` recovers the second method, and disabling knight moves recovers the first. .. [#] Serrà, Joan, Xavier Serra, and Ralph G. Andrzejak. "Cross recurrence quantification for cover song identification." New Journal of Physics 11, no. 9 (2009): 093017. .. [#] Eckmann, J. P., S. Oliffson Kamphorst, and D. Ruelle. "Recurrence plots of dynamical systems." World Scientific Series on Nonlinear Science Series A 16 (1995): 441-446. Parameters ---------- sim : np.ndarray [shape=(N, M), non-negative] The similarity matrix to use as input. This can either be a recurrence matrix (self-similarity) or a cross-similarity matrix between two sequences. gap_onset : float > 0 Penalty for introducing a gap to an alignment sequence gap_extend : float > 0 Penalty for extending a gap in an alignment sequence knight_moves : bool If ``True`` (default), allow for "knight moves" in the alignment, e.g., ``(n, m) => (n + 1, m + 2)`` or ``(n + 2, m + 1)``. If ``False``, only allow for diagonal moves ``(n, m) => (n + 1, m + 1)``. backtrack : bool If ``True``, return the alignment path. If ``False``, only return the score matrix. Returns ------- score : np.ndarray [shape=(N, M)] The alignment score matrix. ``score[n, m]`` is the cumulative value of the best alignment sequence ending in frames ``n`` and ``m``. path : np.ndarray [shape=(k, 2)] (optional) If ``backtrack=True``, ``path`` contains a list of pairs of aligned frames in the best alignment sequence. ``path[i] = [n, m]`` indicates that row ``n`` aligns to column ``m``. See Also -------- librosa.segment.recurrence_matrix librosa.segment.cross_similarity dtw Examples -------- Simple diagonal path enhancement (L-mode) >>> import numpy as np >>> import matplotlib.pyplot as plt >>> y, sr = librosa.loadx('nutcracker', duration=30) >>> chroma = librosa.feature.chroma_cqt(y=y, sr=sr) >>> # Use time-delay embedding to reduce noise >>> chroma_stack = librosa.feature.stack_memory(chroma, n_steps=10, delay=3) >>> # Build recurrence, suppress self-loops within 1 second >>> rec = librosa.segment.recurrence_matrix(chroma_stack, width=43, ... mode='affinity', ... metric='cosine') >>> # using infinite cost for gaps enforces strict path continuation >>> L_score, L_path = librosa.sequence.rqa(rec, ... gap_onset=np.inf, ... gap_extend=np.inf, ... knight_moves=False) >>> fig, ax = plt.subplots(ncols=2) >>> librosa.display.specshow(rec, x_axis='frames', y_axis='frames', ax=ax[0]) >>> ax[0].set(title='Recurrence matrix') >>> librosa.display.specshow(L_score, x_axis='frames', y_axis='frames', ax=ax[1]) >>> hl = librosa.display.highlight(ax=ax[1]) >>> ax[1].set(title='Alignment score matrix') >>> ax[1].plot(L_path[:, 1], L_path[:, 0], label='Optimal path', path_effects=hl) >>> ax[1].legend() >>> ax[1].label_outer() >>> plt.show() Full alignment using gaps and knight moves >>> # New gaps cost 5, extending old gaps cost 10 for each step >>> score, path = librosa.sequence.rqa(rec, gap_onset=5, gap_extend=10) >>> fig, ax = plt.subplots(ncols=2, sharex=True, sharey=True) >>> librosa.display.specshow(rec, x_axis='frames', y_axis='frames', ax=ax[0]) >>> ax[0].set(title='Recurrence matrix') >>> librosa.display.specshow(score, x_axis='frames', y_axis='frames', ax=ax[1]) >>> ax[1].set(title='Alignment score matrix') >>> hl = librosa.display.highlight(ax=ax[1]) >>> ax[1].plot(path[:, 1], path[:, 0], label='Optimal path', path_effects=hl) >>> ax[1].legend() >>> ax[1].label_outer() >>> plt.show() """ if gap_onset < 0: raise ParameterError("gap_onset={} must be strictly positive") if gap_extend < 0: raise ParameterError("gap_extend={} must be strictly positive") score: np.ndarray pointers: np.ndarray score, pointers = __rqa_dp(sim, gap_onset, gap_extend, knight_moves) if backtrack: path = __rqa_backtrack(score, pointers) return score, path return score @jit(nopython=True, cache=True) # type: ignore def __rqa_dp( sim: np.ndarray, gap_onset: float, gap_extend: float, knight: bool ) -> tuple[np.ndarray, npt.NDArray[np.int8]]: # pragma: no cover """RQA dynamic programming implementation""" # The output array score = np.zeros(sim.shape, dtype=sim.dtype) # The backtracking array backtrack = np.zeros(sim.shape, dtype=np.int8) # These are place-holder arrays to limit the points being considered # at each step of the DP # # If knight moves are enabled, values are indexed according to # [(-1,-1), (-1, -2), (-2, -1)] # # If knight moves are disabled, then only the first entry is used. # # Using placeholder vectors here makes the code a bit cleaner down below. sim_values = np.zeros(3) score_values = np.zeros(3) vec = np.zeros(3) if knight: # Initial limit is for the base case: diagonal + one knight init_limit = 2 # Otherwise, we have 3 positions limit = 3 else: init_limit = 1 limit = 1 # backtracking rubric: # 0 ==> diagonal move # 1 ==> knight move up # 2 ==> knight move left # -1 ==> reset without inclusion # -2 ==> reset with inclusion (ie positive value at init) # Initialize the first row and column with the data score[0, :] = sim[0, :] score[:, 0] = sim[:, 0] # backtracking initialization: the first row and column are all resets # if there's a positive link here, it's an inclusive reset for i in range(sim.shape[0]): if sim[i, 0]: backtrack[i, 0] = -2 else: backtrack[i, 0] = -1 for j in range(sim.shape[1]): if sim[0, j]: backtrack[0, j] = -2 else: backtrack[0, j] = -1 if sim.shape[0] == 1 or sim.shape[1] == 1: return score, backtrack # Initialize the 1-1 case using only the diagonal if sim[1, 1] > 0: score[1, 1] = score[0, 0] + sim[1, 1] backtrack[1, 1] = 0 else: link = sim[0, 0] > 0 score[1, 1] = max(0, score[0, 0] - (link) * gap_onset - (~link) * gap_extend) if score[1, 1] > 0: backtrack[1, 1] = 0 else: backtrack[1, 1] = -1 # Initialize the second row with diagonal and left-knight moves i = 1 for j in range(2, sim.shape[1]): score_values[:-1] = (score[i - 1, j - 1], score[i - 1, j - 2]) sim_values[:-1] = (sim[i - 1, j - 1], sim[i - 1, j - 2]) t_values = sim_values > 0 if sim[i, j] > 0: backtrack[i, j] = np.argmax(score_values[:init_limit]) score[i, j] = score_values[backtrack[i, j]] + sim[i, j] # or + 1 for binary else: vec[:init_limit] = ( score_values[:init_limit] - t_values[:init_limit] * gap_onset - (~t_values[:init_limit]) * gap_extend ) backtrack[i, j] = np.argmax(vec[:init_limit]) score[i, j] = max(0, vec[backtrack[i, j]]) # Is it a reset? if score[i, j] == 0: backtrack[i, j] = -1 # Initialize the second column with diagonal and up-knight moves j = 1 for i in range(2, sim.shape[0]): score_values[:-1] = (score[i - 1, j - 1], score[i - 2, j - 1]) sim_values[:-1] = (sim[i - 1, j - 1], sim[i - 2, j - 1]) t_values = sim_values > 0 if sim[i, j] > 0: backtrack[i, j] = np.argmax(score_values[:init_limit]) score[i, j] = score_values[backtrack[i, j]] + sim[i, j] # or + 1 for binary else: vec[:init_limit] = ( score_values[:init_limit] - t_values[:init_limit] * gap_onset - (~t_values[:init_limit]) * gap_extend ) backtrack[i, j] = np.argmax(vec[:init_limit]) score[i, j] = max(0, vec[backtrack[i, j]]) # Is it a reset? if score[i, j] == 0: backtrack[i, j] = -1 # Now fill in the rest of the table for i in range(2, sim.shape[0]): for j in range(2, sim.shape[1]): score_values[:] = ( score[i - 1, j - 1], score[i - 1, j - 2], score[i - 2, j - 1], ) sim_values[:] = (sim[i - 1, j - 1], sim[i - 1, j - 2], sim[i - 2, j - 1]) t_values = sim_values > 0 if sim[i, j] > 0: # if knight is true, it's max of (-1,-1), (-1, -2), (-2, -1) # otherwise, it's just the diagonal move (-1, -1) # for backtracking purposes, if the max is 0 then it's the start of a new sequence # if the max is non-zero, then we extend the existing sequence backtrack[i, j] = np.argmax(score_values[:limit]) score[i, j] = ( score_values[backtrack[i, j]] + sim[i, j] ) # or + 1 for binary else: # if the max of our options is negative, then it's a hard reset # otherwise, it's a skip move vec[:limit] = ( score_values[:limit] - t_values[:limit] * gap_onset - (~t_values[:limit]) * gap_extend ) backtrack[i, j] = np.argmax(vec[:limit]) score[i, j] = max(0, vec[backtrack[i, j]]) # Is it a reset? if score[i, j] == 0: backtrack[i, j] = -1 return score, backtrack def __rqa_backtrack(score, pointers) -> _Array2D[np.uint]: """RQA path backtracking Given the score matrix and backtracking index array, reconstruct the optimal path. """ # backtracking rubric: # 0 ==> diagonal move # 1 ==> knight move up # 2 ==> knight move left # -1 ==> reset (sim = 0) # -2 ==> start of sequence (sim > 0) # This array maps the backtracking values to the # relative index offsets offsets = [(-1, -1), (-1, -2), (-2, -1)] # Find the maximum to end the path idx = list(np.unravel_index(np.argmax(score), score.shape)) # Construct the path path: list = [] while True: bt_index = pointers[tuple(idx)] # A -1 indicates a non-inclusive reset # this can only happen when sim[idx] == 0, # and a reset with zero score should not be included # in the path. In this case, we're done. if bt_index == -1: break # Other bt_index values are okay for inclusion path.insert(0, idx) # -2 indicates beginning of sequence, # so we can't backtrack any further if bt_index == -2: break # Otherwise, prepend this index and continue idx = [idx[_] + offsets[bt_index][_] for _ in range(len(idx))] # If there's no alignment path at all, eg an empty cross-similarity # matrix, return a properly shaped and typed array if not path: return np.empty((0, 2), dtype=np.uint) return np.asarray(path, dtype=np.uint) def path_to_steps(path: np.ndarray, *, inverse: bool = False) -> npt.NDArray[np.float64]: """Convert a DTW warping path to an array of fractional steps. The step sequence is computed by linear interpolation of the warping path indices. This function primarily exists as a helper to construct non-linear time grids for aligning two signals via phase vocoding, as illustrated in the example below. Parameters ---------- path : np.ndarray [shape=(k, 2)] A path of index pairs, e.g., from `dtw`. ``path[i] = [n, m]`` indicates that step ``n`` of the first signal aligns to step ``m`` of the second signal. inverse : bool If ``False`` (default), the output is a time grid mapping the first sequence (first column of ``path``) to the second sequence. If ``True``, the output is a time grid mapping the second sequence (second column of ``path``) to the first sequence. Returns ------- steps : np.ndarray [shape=(t,)] An array of fractional steps, where ``steps[i]`` is the index in the first sequence corresponding to the ``i`` th step of the second sequence. The number of steps is determined by the range of the target sequence indices. See Also -------- dtw librosa.phase_vocoder Examples -------- This example generates a sine sweep over the same frequency range but at different speeds. It then uses dynamic time warping to align the chroma features of the two signals. Finally, the warping path is converted to a time grid that maps the second signal's frames onto the first signal. This can be used for variable-rate phase vocoding to align the two signals. >>> # We'll plot these below >>> import matplotlib.pyplot as plt >>> # Generate the sweeps at different rates >>> y1 = librosa.chirp(fmin=220, fmax=1760, duration=1.0, sr=22050) >>> y2 = librosa.chirp(fmin=220, fmax=1760, duration=2.0, sr=22050) >>> # Compute the chroma features >>> C1 = librosa.feature.chroma_cqt(y=y1, sr=22050) >>> C2 = librosa.feature.chroma_cqt(y=y2, sr=22050) >>> # Compute the DTW path >>> cost, path = librosa.sequence.dtw(C1, C2, subseq=False) >>> path array([[43, 86], [42, 85], [42, 84], ..., [ 0, 2], [ 0, 1], [ 0, 0]], shape=(87, 2)) >>> # Convert the path to a fractional step grid >>> steps = librosa.sequence.path_to_steps(path) >>> steps array([ 0., 0., 0., ..., 42., 42., 43.], shape=(87,)) >>> # Compute STFT for the first signal >>> D1 = librosa.stft(y1) >>> # Phase vocode it to match the second signal using the time grid >>> D1_vocoded = librosa.phase_vocoder(D1, t_out=steps) >>> # Map back into the time domain. Match duration of y2 exactly >>> y1_vocoded = librosa.istft(D1_vocoded, length=len(y2)) >>> fig, ax = plt.subplots(nrows=3, sharex=True, sharey=True) >>> librosa.display.specshow(C1, x_axis='time', y_axis='chroma', ax=ax[0]) >>> ax[0].set(title='Chroma 1: fast') >>> librosa.display.specshow(C2, x_axis='time', y_axis='chroma', ax=ax[1]) >>> ax[1].set(title='Chroma 2: slow') >>> C3 = librosa.feature.chroma_cqt(y=y1_vocoded, sr=22050) >>> librosa.display.specshow(C3, x_axis='time', y_axis='chroma', ax=ax[2]) >>> ax[2].set(title='Chroma 1 vocoded to match Chroma 2') """ if inverse: idx_from, idx_to = 1, 0 else: idx_from, idx_to = 0, 1 import scipy.interpolate step_interp = scipy.interpolate.interp1d( path[:, idx_to], path[:, idx_from], kind="linear" ) frames_in = np.arange(path[:, idx_to].min(), path[:, idx_to].max() + 1) steps = step_interp(frames_in) return cast("npt.NDArray[np.float64]", steps) @jit(nopython=True, cache=True) # type: ignore def _viterbi( log_prob: np.ndarray, log_trans: np.ndarray, log_p_init: np.ndarray, log_trans_threshold: float, ) -> tuple[npt.NDArray[np.uint16], npt.NDArray[np.float64]]: # pragma: no cover """Core Viterbi algorithm. This is intended for internal use only. Parameters ---------- log_prob : np.ndarray [shape=(T, m)] ``log_prob[t, s]`` is the conditional log-likelihood ``log P[X = X(t) | State(t) = s]`` log_trans : np.ndarray [shape=(m, m)] The log transition matrix ``log_trans[i, j] = log P[State(t+1) = j | State(t) = i]`` log_p_init : np.ndarray [shape=(m,)] log of the initial state distribution log_trans_threshold : float threshold above which transitions are considered feasible. Use -np.inf to perform a full search (exact viterbi). Otherwise, only transitions with (log) probability above the threshold are considered in the loop. Returns ------- None All computations are performed in-place on ``state, value, ptr``. """ n_steps, n_states = log_prob.shape state = np.zeros(n_steps, dtype=np.uint16) value = np.zeros((n_steps, n_states), dtype=np.float64) ptr = np.zeros((n_steps, n_states), dtype=np.uint16) # factor in initial state distribution value[0] = log_prob[0] + log_p_init if np.isfinite(log_trans_threshold): pred_states = [] for j in range(n_states): possible_states = np.flatnonzero(log_trans[:, j] >= log_trans_threshold) if len(possible_states) == 0: raise ParameterError( f"Empty transition matrix detected for state {j} in Viterbi. " f"Try reducing your minimum transition probability threshold." ) pred_states.append(possible_states) for t in range(1, n_steps): # Want V[t, j] <- p[t, j] * max_k V[t-1, k] * A[k, j] # assume at time t-1 we were in state k # transition k -> j for j in range(n_states): best_cost = -np.inf for k in pred_states[j]: cost = value[t - 1, k] + log_trans[k, j] if cost > best_cost: ptr[t, j] = k best_cost = cost value[t, j] = log_prob[t, j] + best_cost else: # Same as above, but doing a full search over previous states for t in range(1, n_steps): for j in range(n_states): best_cost = -np.inf for kj in range(n_states): cost = value[t - 1, kj] + log_trans[kj, j] if cost > best_cost: ptr[t, j] = kj best_cost = cost value[t, j] = log_prob[t, j] + best_cost # Now roll backward # Get the last state state[-1] = np.argmax(value[-1]) for t in range(n_steps - 2, -1, -1): state[t] = ptr[t + 1, state[t + 1]] logp = value[-1:, state[-1]] return state, logp @overload def viterbi( prob: np.ndarray, transition: np.ndarray, *, p_init: np.ndarray | None = ..., return_logp: Literal[True], transition_min_prob: float | None = ..., ) -> tuple[npt.NDArray[np.uint16], npt.NDArray[np.float64]]: ... @overload def viterbi( prob: np.ndarray, transition: np.ndarray, *, p_init: np.ndarray | None = ..., return_logp: Literal[False] = ..., transition_min_prob: float | None = ..., ) -> npt.NDArray[np.uint16]: ... def viterbi( prob: np.ndarray, transition: np.ndarray, *, p_init: np.ndarray | None = None, return_logp: bool = False, transition_min_prob: float | None = None, ) -> npt.NDArray[np.uint16] | tuple[npt.NDArray[np.uint16], npt.NDArray[np.float64]]: """Viterbi decoding from observation likelihoods. Given a sequence of observation likelihoods ``prob[s, t]``, indicating the conditional likelihood of seeing the observation at time ``t`` from state ``s``, and a transition matrix ``transition[i, j]`` which encodes the conditional probability of moving from state ``i`` to state ``j``, the Viterbi algorithm [#]_ computes the most likely sequence of states from the observations. .. [#] Viterbi, Andrew. "Error bounds for convolutional codes and an asymptotically optimum decoding algorithm." IEEE transactions on Information Theory 13.2 (1967): 260-269. Parameters ---------- prob : np.ndarray [shape=(..., n_states, n_steps), non-negative] ``prob[..., s, t]`` is the probability of observation at time ``t`` being generated by state ``s``. transition : np.ndarray [shape=(n_states, n_states), non-negative] ``transition[i, j]`` is the probability of a transition from i->j. Each row must sum to 1. p_init : np.ndarray [shape=(n_states,)] Optional: initial state distribution. If not provided, a uniform distribution is assumed. return_logp : bool If ``True``, return the log-likelihood of the state sequence. transition_min_prob : float, >=0 Optional: if provided, the minimum transition probability to consider during decoding. Returns ------- states : np.ndarray [shape=(..., n_steps,)] The most likely state sequence. If ``prob`` contains multiple channels of input, then each channel is decoded independently. logp : scalar [float] or np.ndarray, optional If ``return_logp=True``, the log probability of ``states`` given the observations. See Also -------- viterbi_discriminative : Viterbi decoding from state likelihoods Examples -------- Example from https://en.wikipedia.org/wiki/Viterbi_algorithm#Example In this example, we have two states ``healthy`` and ``fever``, with initial probabilities 60% and 40%. We have three observation possibilities: ``normal``, ``cold``, and ``dizzy``, whose probabilities given each state are: ``healthy => {normal: 50%, cold: 40%, dizzy: 10%}`` and ``fever => {normal: 10%, cold: 30%, dizzy: 60%}`` Finally, we have transition probabilities: ``healthy => healthy (70%)`` and ``fever => fever (60%)``. Over three days, we observe the sequence ``[normal, cold, dizzy]``, and wish to know the maximum likelihood assignment of states for the corresponding days, which we compute with the Viterbi algorithm below. >>> p_init = np.array([0.6, 0.4]) >>> p_emit = np.array([[0.5, 0.4, 0.1], ... [0.1, 0.3, 0.6]]) >>> p_trans = np.array([[0.7, 0.3], [0.4, 0.6]]) >>> path, logp = librosa.sequence.viterbi(p_emit, p_trans, p_init=p_init, ... return_logp=True) >>> print(logp, path) -4.19173690823075 [0 0 1] """ n_states, _n_steps = prob.shape[-2:] if transition.shape != (n_states, n_states): raise ParameterError( f"transition.shape={transition.shape}, must be " f"(n_states, n_states)={n_states, n_states}" ) if np.any(transition < 0) or not np.allclose(transition.sum(axis=1), 1): raise ParameterError( "Invalid transition matrix: must be non-negative " "and sum to 1 on each row." ) if np.any(prob < 0) or np.any(prob > 1): raise ParameterError("Invalid probability values: must be between 0 and 1.") # Compute log-likelihoods while avoiding log-underflow epsilon = tiny(prob) if p_init is None: p_init = np.empty(n_states) p_init.fill(1.0 / n_states) elif ( np.any(p_init < 0) or not np.allclose(p_init.sum(), 1) or p_init.shape != (n_states,) ): raise ParameterError(f"Invalid initial state distribution: p_init={p_init}") log_trans = np.log(transition + epsilon) log_prob = np.log(prob + epsilon) log_p_init = np.log(p_init + epsilon) if transition_min_prob is not None and transition_min_prob > 0: log_trans_threshold = np.log(transition_min_prob + epsilon) elif transition_min_prob is None or transition_min_prob == 0: log_trans_threshold = -np.inf else: raise ParameterError( f"Invalid transition_min_prob={transition_min_prob}, must be None or non-negative." ) def _helper(lp) -> tuple[npt.NDArray[np.uint16], npt.NDArray[np.float64]]: # Transpose input _state, logp = _viterbi(lp.T, log_trans, log_p_init, log_trans_threshold) # Transpose outputs for return return _state.T, logp states: np.ndarray logp: np.ndarray if log_prob.ndim == 2: states, logp = _helper(log_prob) else: # Vectorize the helper __viterbi = np.vectorize( _helper, otypes=[np.uint16, np.float64], signature="(s,t)->(t),(1)" ) states, logp = __viterbi(log_prob) # Flatten out the trailing dimension introduced by vectorization logp = logp[..., 0] if return_logp: return states, logp return states @overload def viterbi_discriminative( prob: np.ndarray, transition: np.ndarray, *, p_state: np.ndarray | None = ..., p_init: np.ndarray | None = ..., return_logp: Literal[False] = ..., transition_min_prob: float | None = ..., ) -> npt.NDArray[np.uint16]: ... @overload def viterbi_discriminative( prob: np.ndarray, transition: np.ndarray, *, p_state: np.ndarray | None = ..., p_init: np.ndarray | None = ..., return_logp: Literal[True], transition_min_prob: float | None = ..., ) -> tuple[npt.NDArray[np.uint16], npt.NDArray[np.float64]]: ... def viterbi_discriminative( prob: np.ndarray, transition: np.ndarray, *, p_state: np.ndarray | None = None, p_init: np.ndarray | None = None, return_logp: bool = False, transition_min_prob: float | None = None, ) -> npt.NDArray[np.uint16] | tuple[npt.NDArray[np.uint16], npt.NDArray[np.float64]]: """Viterbi decoding from discriminative state predictions. Given a sequence of conditional state predictions ``prob[s, t]``, indicating the conditional likelihood of state ``s`` given the observation at time ``t``, and a transition matrix ``transition[i, j]`` which encodes the conditional probability of moving from state ``i`` to state ``j``, the Viterbi algorithm computes the most likely sequence of states from the observations. This implementation uses the standard Viterbi decoding algorithm for observation likelihood sequences, under the assumption that ``P[Obs(t) | State(t) = s]`` is proportional to ``P[State(t) = s | Obs(t)] / P[State(t) = s]``, where the denominator is the marginal probability of state ``s`` occurring as given by ``p_state``. Note that because the denominator ``P[State(t) = s]`` is not explicitly calculated, the resulting probabilities (or log-probabilities) are not normalized. If using the `return_logp=True` option (see below), be aware that the "probabilities" may not sum to (and may exceed) 1. Parameters ---------- prob : np.ndarray [shape=(..., n_states, n_steps), non-negative] ``prob[s, t]`` is the probability of state ``s`` conditional on the observation at time ``t``. Must be non-negative and sum to 1 along each column. transition : np.ndarray [shape=(n_states, n_states), non-negative] ``transition[i, j]`` is the probability of a transition from i->j. Each row must sum to 1. p_state : np.ndarray [shape=(n_states,)] Optional: marginal probability distribution over states, must be non-negative and sum to 1. If not provided, a uniform distribution is assumed. p_init : np.ndarray [shape=(n_states,)] Optional: initial state distribution. If not provided, it is assumed to be uniform. return_logp : bool If ``True``, return the log-likelihood of the state sequence. transition_min_prob : float, >= 0 Optional: if provided, the minimum transition probability to consider during decoding. Returns ------- states : np.ndarray [shape=(..., n_steps,)] The most likely state sequence. If ``prob`` contains multiple input channels, then each channel is decoded independently. logp : scalar [float] or np.ndarray If ``return_logp=True``, the (unnormalized) log probability of ``states`` given the observations. See Also -------- viterbi : Viterbi decoding from observation likelihoods viterbi_binary : Viterbi decoding for multi-label, conditional state likelihoods Examples -------- This example constructs a simple, template-based discriminative chord estimator, using CENS chroma as input features. .. note:: this chord model is not accurate enough to use in practice. It is only intended to demonstrate how to use discriminative Viterbi decoding. >>> # Create templates for major, minor, and no-chord qualities >>> maj_template = np.array([1,0,0, 0,1,0, 0,1,0, 0,0,0]) >>> min_template = np.array([1,0,0, 1,0,0, 0,1,0, 0,0,0]) >>> N_template = np.array([1,1,1, 1,1,1, 1,1,1, 1,1,1.]) / 4. >>> # Generate the weighting matrix that maps chroma to labels >>> weights = np.zeros((25, 12), dtype=float) >>> labels = ['C:maj', 'C#:maj', 'D:maj', 'D#:maj', 'E:maj', 'F:maj', ... 'F#:maj', 'G:maj', 'G#:maj', 'A:maj', 'A#:maj', 'B:maj', ... 'C:min', 'C#:min', 'D:min', 'D#:min', 'E:min', 'F:min', ... 'F#:min', 'G:min', 'G#:min', 'A:min', 'A#:min', 'B:min', ... 'N'] >>> for c in range(12): ... weights[c, :] = np.roll(maj_template, c) # c:maj ... weights[c + 12, :] = np.roll(min_template, c) # c:min >>> weights[-1] = N_template # the last row is the no-chord class >>> # Make a self-loop transition matrix over 25 states >>> trans = librosa.sequence.transition_loop(25, 0.9) >>> # Load in audio and make features >>> y, sr = librosa.loadx('nutcracker', duration=15) >>> # Suppress percussive elements >>> y = librosa.effects.harmonic(y, margin=4) >>> chroma = librosa.feature.chroma_cqt(y=y, sr=sr, threshold=0.25) >>> # Map chroma (observations) to class (state) likelihoods >>> probs = np.exp(weights.dot(chroma)) # P[class | chroma] ~= exp(template' chroma) >>> probs /= probs.sum(axis=0, keepdims=True) # probabilities must sum to 1 in each column >>> # Compute independent frame-wise estimates >>> chords_ind = np.argmax(probs, axis=0) >>> # And viterbi estimates >>> chords_vit = librosa.sequence.viterbi_discriminative(probs, trans) >>> # Plot the features and prediction map >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=2) >>> librosa.display.specshow(chroma, x_axis='time', y_axis='chroma', ax=ax[0]) >>> librosa.display.specshow(weights, x_axis='chroma', ax=ax[1]) >>> ax[1].set(yticks=np.arange(25) + 0.5, yticklabels=labels, ylabel='Chord') >>> plt.show() >>> # And plot the results >>> fig, ax = plt.subplots() >>> img = librosa.display.specshow(probs, x_axis='time', ax=ax, div_thresh=1./25) >>> hl = librosa.display.highlight(ax=ax, alpha=0.1) >>> fig.colorbar(img, ax=ax, label='Frame-wise class probability') >>> times = librosa.times_like(chords_vit) >>> ax.scatter(times, chords_ind + 0.25, alpha=0.85, marker='x', color='k', ... s=15, label='Independent', path_effects=hl) >>> ax.scatter(times, chords_vit - 0.25, alpha=0.85, marker='o', color='g', ... s=15, label='Viterbi', path_effects=hl) >>> ax.set(yticks=np.unique(chords_vit), ... yticklabels=[labels[i] for i in np.unique(chords_vit)]) >>> ax.legend(loc='upper right', ncols=2) >>> plt.show() """ n_states, _n_steps = prob.shape[-2:] if transition.shape != (n_states, n_states): raise ParameterError( f"transition.shape={transition.shape}, must be " f"(n_states, n_states)={n_states, n_states}" ) if np.any(transition < 0) or not np.allclose(transition.sum(axis=1), 1): raise ParameterError( "Invalid transition matrix: must be non-negative " "and sum to 1 on each row." ) if np.any(prob < 0) or not np.allclose(prob.sum(axis=-2), 1): raise ParameterError( "Invalid probability values: each column must " "sum to 1 and be non-negative" ) # Compute log-likelihoods while avoiding log-underflow epsilon = tiny(prob) # Compute marginal log probabilities while avoiding underflow if p_state is None: p_state = np.empty(n_states) p_state.fill(1.0 / n_states) elif p_state.shape != (n_states,): raise ParameterError( "Marginal distribution p_state must have shape (n_states,). " f"Got p_state.shape={p_state.shape}" ) elif np.any(p_state < 0) or not np.allclose(p_state.sum(axis=-1), 1): raise ParameterError(f"Invalid marginal state distribution: p_state={p_state}") if p_init is None: p_init = np.empty(n_states) p_init.fill(1.0 / n_states) elif ( np.any(p_init < 0) or not np.allclose(p_init.sum(), 1) or p_init.shape != (n_states,) ): raise ParameterError(f"Invalid initial state distribution: p_init={p_init}") # By Bayes' rule, P[X | Y] * P[Y] = P[Y | X] * P[X] # P[X] is constant for the sake of maximum likelihood inference # and P[Y] is given by the marginal distribution p_state. # # So we have P[X | y] \propto P[Y | x] / P[Y] # if X = observation and Y = states, this can be done in log space as # log P[X | y] \propto \log P[Y | x] - \log P[Y] log_p_init = np.log(p_init + epsilon) log_trans = np.log(transition + epsilon) log_marginal = np.log(p_state + epsilon) if transition_min_prob is not None and transition_min_prob > 0: log_trans_threshold = np.log(transition_min_prob + epsilon) elif transition_min_prob is None or transition_min_prob == 0: log_trans_threshold = -np.inf else: raise ParameterError( f"Invalid transition_min_prob={transition_min_prob}, must be None or non-negative." ) # reshape to broadcast against prob log_marginal = expand_to(log_marginal, ndim=prob.ndim, axes=-2) log_prob = np.log(prob + epsilon) - log_marginal def _helper(lp): # Transpose input _state, logp = _viterbi(lp.T, log_trans, log_p_init, log_trans_threshold) # Transpose outputs for return return _state.T, logp states: np.ndarray logp: np.ndarray if log_prob.ndim == 2: states, logp = _helper(log_prob) else: # Vectorize the helper __viterbi = np.vectorize( _helper, otypes=[np.uint16, np.float64], signature="(s,t)->(t),(1)" ) states, logp = __viterbi(log_prob) # Flatten out the trailing dimension logp = logp[..., 0] if return_logp: return states, logp return states @overload def viterbi_binary( prob: np.ndarray, transition: np.ndarray, *, p_state: np.ndarray | None = ..., p_init: np.ndarray | None = ..., return_logp: Literal[False] = ..., transition_min_prob: float | None = ..., ) -> npt.NDArray[np.uint16]: ... @overload def viterbi_binary( prob: np.ndarray, transition: np.ndarray, *, p_state: np.ndarray | None = ..., p_init: np.ndarray | None = ..., return_logp: Literal[True], transition_min_prob: float | None = ..., ) -> tuple[npt.NDArray[np.uint16], npt.NDArray[np.float64]]: ... def viterbi_binary( prob: np.ndarray, transition: np.ndarray, *, p_state: np.ndarray | None = None, p_init: np.ndarray | None = None, return_logp: bool = False, transition_min_prob: float | None = None, ) -> npt.NDArray[np.uint16] | tuple[npt.NDArray[np.uint16], npt.NDArray[np.float64]]: """Viterbi decoding from binary (multi-label), discriminative state predictions. Given a sequence of conditional state predictions ``prob[s, t]``, indicating the conditional likelihood of state ``s`` being active conditional on observation at time ``t``, and a 2*2 transition matrix ``transition`` which encodes the conditional probability of moving from state ``s`` to state ``~s`` (not-``s``), the Viterbi algorithm computes the most likely sequence of states from the observations. This function differs from `viterbi_discriminative` in that it does not assume the states to be mutually exclusive. `viterbi_binary` is implemented by transforming the multi-label decoding problem to a collection of binary Viterbi problems (one for each *state* or label). The output is a binary matrix ``states[s, t]`` indicating whether each state ``s`` is active at time ``t``. Like `viterbi_discriminative`, the probabilities of the optimal state sequences are not normalized here. If using the `return_logp=True` option (see below), be aware that the "probabilities" may not sum to (and may exceed) 1. Parameters ---------- prob : np.ndarray [shape=(..., n_steps,) or (..., n_states, n_steps)], non-negative ``prob[s, t]`` is the probability of state ``s`` being active conditional on the observation at time ``t``. Must be non-negative and less than 1. If ``prob`` is 1-dimensional, it is expanded to shape ``(1, n_steps)``. If ``prob`` contains multiple input channels, then each channel is decoded independently. transition : np.ndarray [shape=(2, 2) or (n_states, 2, 2)], non-negative If 2-dimensional, the same transition matrix is applied to each sub-problem. ``transition[0, i]`` is the probability of the state going from inactive to ``i``, ``transition[1, i]`` is the probability of the state going from active to ``i``. Each row must sum to 1. If 3-dimensional, ``transition[s]`` is interpreted as the 2x2 transition matrix for state label ``s``. p_state : np.ndarray [shape=(n_states,)] Optional: marginal probability for each state (between [0,1]). If not provided, a uniform distribution (0.5 for each state) is assumed. p_init : np.ndarray [shape=(n_states,)] Optional: initial state distribution. If not provided, it is assumed to be uniform. return_logp : bool If ``True``, return the (unnormalized) log-likelihood of the state sequences. transition_min_prob : float, >= 0 Optional: if provided, the minimum transition probability to consider during decoding. Returns ------- states : np.ndarray [shape=(..., n_states, n_steps)] The most likely state sequence. logp : np.ndarray [shape=(..., n_states,)] If ``return_logp=True``, the (unnormalized) log probability of each state activation sequence ``states`` See Also -------- viterbi : Viterbi decoding from observation likelihoods viterbi_discriminative : Viterbi decoding for discriminative (mutually exclusive) state predictions Examples -------- In this example, we have a sequence of binary state likelihoods that we want to de-noise under the assumption that state changes are relatively uncommon. Positive predictions should only be retained if they persist for multiple steps, and any transient predictions should be considered as errors. This use case arises frequently in problems such as instrument recognition, where state activations tend to be stable over time, but subject to abrupt changes (e.g., when an instrument joins the mix). We assume that the 0 state has a self-transition probability of 90%, and the 1 state has a self-transition probability of 70%. We assume the marginal and initial probability of either state is 50%. >>> trans = np.array([[0.9, 0.1], [0.3, 0.7]]) >>> prob = np.array([0.1, 0.7, 0.4, 0.3, 0.8, 0.9, 0.8, 0.2, 0.6, 0.3]) >>> librosa.sequence.viterbi_binary(prob, trans, p_state=0.5, p_init=0.5) array([[0, 0, 0, 0, 1, 1, 1, 0, 0, 0]]) """ prob = np.atleast_2d(prob) n_states, n_steps = prob.shape[-2:] if transition.shape == (2, 2): transition = np.tile(transition, (n_states, 1, 1)) elif transition.shape != (n_states, 2, 2): raise ParameterError( f"transition.shape={transition.shape}, must be (2, 2) or " f"(n_states, 2, 2)={n_states}" ) if np.any(transition < 0) or not np.allclose(transition.sum(axis=-1), 1): raise ParameterError( "Invalid transition matrix: must be non-negative " "and sum to 1 on each row." ) if np.any(prob < 0) or np.any(prob > 1): raise ParameterError("Invalid probability values: prob must be between [0, 1]") if p_state is None: p_state = np.empty(n_states) p_state.fill(0.5) else: p_state = np.atleast_1d(p_state) assert p_state is not None if p_state.shape != (n_states,) or np.any(p_state < 0) or np.any(p_state > 1): raise ParameterError(f"Invalid marginal state distributions: p_state={p_state}") if p_init is None: p_init = np.empty(n_states) p_init.fill(0.5) else: p_init = np.atleast_1d(p_init) assert p_init is not None if p_init.shape != (n_states,) or np.any(p_init < 0) or np.any(p_init > 1): raise ParameterError(f"Invalid initial state distributions: p_init={p_init}") shape_prefix = list(prob.shape[:-2]) states = np.empty([*shape_prefix, n_states, n_steps], dtype=np.uint16) logp = np.empty([*shape_prefix, n_states]) prob_binary = np.empty([*shape_prefix, 2, n_steps]) p_state_binary = np.empty(2) p_init_binary = np.empty(2) for state in range(n_states): prob_binary[..., 0, :] = 1 - prob[..., state, :] prob_binary[..., 1, :] = prob[..., state, :] p_state_binary[0] = 1 - p_state[state] p_state_binary[1] = p_state[state] p_init_binary[0] = 1 - p_init[state] p_init_binary[1] = p_init[state] states[..., state, :], logp[..., state] = viterbi_discriminative( prob_binary, transition[state], p_state=p_state_binary, p_init=p_init_binary, return_logp=True, transition_min_prob=transition_min_prob, ) if return_logp: return states, logp return states def transition_uniform(n_states: int) -> _Array2D[np.float64]: """Construct a uniform transition matrix over ``n_states``. Parameters ---------- n_states : int > 0 The number of states Returns ------- transition : np.ndarray [shape=(n_states, n_states)] ``transition[i, j] = 1./n_states`` Examples -------- >>> librosa.sequence.transition_uniform(3) array([[0.333, 0.333, 0.333], [0.333, 0.333, 0.333], [0.333, 0.333, 0.333]]) """ if not is_positive_int(n_states): raise ParameterError(f"n_states={n_states} must be a positive integer") transition = np.empty((n_states, n_states), dtype=np.float64) transition.fill(1.0 / n_states) return transition def transition_loop(n_states: int, prob: float | Iterable[float]) -> _Array2D[np.float64]: """Construct a self-loop transition matrix over ``n_states``. The transition matrix will have the following properties: - ``transition[i, i] = p`` for all ``i`` - ``transition[i, j] = (1 - p) / (n_states - 1)`` for all ``j != i`` This type of transition matrix is appropriate when states tend to be locally stable, and there is no additional structure between different states. This is primarily useful for de-noising frame-wise predictions. Parameters ---------- n_states : int > 1 The number of states prob : float in [0, 1] or iterable, length=n_states If a scalar, this is the probability of a self-transition. If a vector of length ``n_states``, ``p[i]`` is the probability of self-transition in state ``i`` Returns ------- transition : np.ndarray [shape=(n_states, n_states)] The transition matrix Examples -------- >>> librosa.sequence.transition_loop(3, 0.5) array([[0.5 , 0.25, 0.25], [0.25, 0.5 , 0.25], [0.25, 0.25, 0.5 ]]) >>> librosa.sequence.transition_loop(3, [0.8, 0.5, 0.25]) array([[0.8 , 0.1 , 0.1 ], [0.25 , 0.5 , 0.25 ], [0.375, 0.375, 0.25 ]]) """ if not (is_positive_int(n_states) and (n_states > 1)): raise ParameterError(f"n_states={n_states} must be a positive integer > 1") transition = np.empty((n_states, n_states), dtype=np.float64) # if it's a float, make it a vector prob = np.asarray(prob, dtype=np.float64) if prob.ndim == 0: prob = np.tile(prob, n_states) if prob.shape != (n_states,): raise ParameterError( f"prob={prob} must have length equal to n_states={n_states}" ) if np.any(prob < 0) or np.any(prob > 1): raise ParameterError(f"prob={prob} must have values in the range [0, 1]") for i, prob_i in enumerate(prob): transition[i] = (1.0 - prob_i) / (n_states - 1) transition[i, i] = prob_i return transition def transition_cycle(n_states: int, prob: float | Iterable[float]) -> _Array2D[np.float64]: """Construct a cyclic transition matrix over ``n_states``. The transition matrix will have the following properties: - ``transition[i, i] = p`` - ``transition[i, i + 1] = (1 - p)`` This type of transition matrix is appropriate for state spaces with cyclical structure, such as metrical position within a bar. For example, a song in 4/4 time has state transitions of the form 1->{1, 2}, 2->{2, 3}, 3->{3, 4}, 4->{4, 1}. Parameters ---------- n_states : int > 1 The number of states prob : float in [0, 1] or iterable, length=n_states If a scalar, this is the probability of a self-transition. If a vector of length ``n_states``, ``p[i]`` is the probability of self-transition in state ``i`` Returns ------- transition : np.ndarray [shape=(n_states, n_states)] The transition matrix Examples -------- >>> librosa.sequence.transition_cycle(4, 0.9) array([[0.9, 0.1, 0. , 0. ], [0. , 0.9, 0.1, 0. ], [0. , 0. , 0.9, 0.1], [0.1, 0. , 0. , 0.9]]) """ if not (is_positive_int(n_states) and n_states > 1): raise ParameterError(f"n_states={n_states} must be a positive integer > 1") transition = np.zeros((n_states, n_states), dtype=np.float64) # if it's a float, make it a vector prob = np.asarray(prob, dtype=np.float64) if prob.ndim == 0: prob = np.tile(prob, n_states) if prob.shape != (n_states,): raise ParameterError( f"prob={prob} must have length equal to n_states={n_states}" ) if np.any(prob < 0) or np.any(prob > 1): raise ParameterError(f"prob={prob} must have values in the range [0, 1]") for i, prob_i in enumerate(prob): transition[i, np.mod(i + 1, n_states)] = 1.0 - prob_i transition[i, i] = prob_i return transition def transition_local( n_states: int, width: int | Iterable[int], *, window: _WindowSpec = "triangle", wrap: bool = False, ) -> _Array2D[np.float64]: """Construct a localized transition matrix. The transition matrix will have the following properties: - ``transition[i, j] = 0`` if ``|i - j| > width`` - ``transition[i, i]`` is maximal - ``transition[i, i - width//2 : i + width//2]`` has shape ``window`` This type of transition matrix is appropriate for state spaces that discretely approximate continuous variables, such as in fundamental frequency estimation. Parameters ---------- n_states : int > 1 The number of states width : int >= 1 or iterable The maximum number of states to treat as "local". If iterable, it should have length equal to ``n_states``, and specify the width independently for each state. window : str, callable, or window specification The window function to determine the shape of the "local" distribution. Any window specification supported by `filters.get_window` will work here. .. note:: Certain windows (e.g., 'hann') are identically 0 at the boundaries, so and effectively have ``width-2`` non-zero values. You may have to expand ``width`` to get the desired behavior. wrap : bool If ``True``, then state locality ``|i - j|`` is computed modulo ``n_states``. If ``False`` (default), then locality is absolute. See Also -------- librosa.filters.get_window Returns ------- transition : np.ndarray [shape=(n_states, n_states)] The transition matrix Examples -------- Triangular distributions with and without wrapping >>> librosa.sequence.transition_local(5, 3, window='triangle', wrap=False) array([[0.667, 0.333, 0. , 0. , 0. ], [0.25 , 0.5 , 0.25 , 0. , 0. ], [0. , 0.25 , 0.5 , 0.25 , 0. ], [0. , 0. , 0.25 , 0.5 , 0.25 ], [0. , 0. , 0. , 0.333, 0.667]]) >>> librosa.sequence.transition_local(5, 3, window='triangle', wrap=True) array([[0.5 , 0.25, 0. , 0. , 0.25], [0.25, 0.5 , 0.25, 0. , 0. ], [0. , 0.25, 0.5 , 0.25, 0. ], [0. , 0. , 0.25, 0.5 , 0.25], [0.25, 0. , 0. , 0.25, 0.5 ]]) Uniform local distributions with variable widths and no wrapping >>> librosa.sequence.transition_local(5, [1, 2, 3, 3, 1], window='ones', wrap=False) array([[1. , 0. , 0. , 0. , 0. ], [0.5 , 0.5 , 0. , 0. , 0. ], [0. , 0.333, 0.333, 0.333, 0. ], [0. , 0. , 0.333, 0.333, 0.333], [0. , 0. , 0. , 0. , 1. ]]) """ if not (is_positive_int(n_states) and n_states > 1): raise ParameterError(f"n_states={n_states} must be a positive integer > 1") width = np.asarray(width, dtype=int) if width.ndim == 0: width = np.tile(width, n_states) if width.shape != (n_states,): raise ParameterError( f"width={width} must have length equal to n_states={n_states}" ) if np.any(width < 1): raise ParameterError(f"width={width} must be at least 1") transition = np.zeros((n_states, n_states), dtype=np.float64) # Fill in the widths. This is inefficient, but simple for i, width_i in enumerate(width): trans_row = pad_center( get_window(window, width_i, fftbins=False), size=n_states ) trans_row = np.roll(trans_row, n_states // 2 + i + 1) if not wrap: # Knock out the off-diagonal-band elements trans_row[min(n_states, i + width_i // 2 + 1) :] = 0 trans_row[: max(0, i - width_i // 2)] = 0 transition[i] = trans_row # Row-normalize transition /= transition.sum(axis=1, keepdims=True) return transition ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462640.9852107 librosa-1.0.0/librosa/util/0000755000175100017510000000000015236640661015232 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2125447 librosa-1.0.0/librosa/util/__init__.py0000644000175100017510000000111615236640653017343 0ustar00runnerrunner#!/usr/bin/env python # -*- coding: utf-8 -*- """ librosa.util contains a variety of utility functions used throughout the package. Utility functions are broadly categorized into the following groups: - Numerical operations (value-based processing, optimization, etc.) - Array shape manipulation (slicing, framing, padding, etc.). - Matching (events or time intervals) - Files and data (packaged examples, metadata, etc.) - Data validation (type and range checking) """ import lazy_loader as lazy __getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2139711 librosa-1.0.0/librosa/util/__init__.pyi0000644000175100017510000000356315236640653017524 0ustar00runnerrunnerfrom . import decorators, exceptions from ._nnls import nnls as nnls from .deprecation import Deprecated as Deprecated from .deprecation import rename_kw as rename_kw from .files import cite as cite from .files import ex as ex from .files import example as example from .files import example_info as example_info from .files import find_files as find_files from .files import list_examples as list_examples from .matching import match_events as match_events from .matching import match_intervals as match_intervals from .utils import MAX_MEM_BLOCK as MAX_MEM_BLOCK from .utils import abs2 as abs2 from .utils import axis_sort as axis_sort from .utils import buf_to_float as buf_to_float from .utils import count_unique as count_unique from .utils import cyclic_gradient as cyclic_gradient from .utils import dtype_c2r as dtype_c2r from .utils import dtype_r2c as dtype_r2c from .utils import expand_to as expand_to from .utils import fill_off_diagonal as fill_off_diagonal from .utils import fix_frames as fix_frames from .utils import fix_length as fix_length from .utils import frame as frame from .utils import index_to_slice as index_to_slice from .utils import interp_broadcast as interp_broadcast from .utils import is_positive_int as is_positive_int from .utils import is_unique as is_unique from .utils import localmax as localmax from .utils import localmin as localmin from .utils import normalize as normalize from .utils import pad_center as pad_center from .utils import peak_pick as peak_pick from .utils import phasor as phasor from .utils import shear as shear from .utils import softmask as softmask from .utils import sparsify_rows as sparsify_rows from .utils import stack as stack from .utils import sync as sync from .utils import tiny as tiny from .utils import valid_audio as valid_audio from .utils import valid_int as valid_int from .utils import valid_intervals as valid_intervals ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2139711 librosa-1.0.0/librosa/util/_nnls.py0000644000175100017510000001246415236640653016725 0ustar00runnerrunner#!/usr/bin/env python # -*- coding: utf-8 -*- """Non-negative least squares""" # The scipy library provides an nnls solver, but it does # not generalize efficiently to matrix-valued problems. # We therefore provide an alternate solver here. # # The vectorized solver uses the L-BFGS-B over blocks of # data to efficiently solve the constrained least-squares problem. from __future__ import annotations from typing import TYPE_CHECKING import numpy as np from .utils import MAX_MEM_BLOCK if TYPE_CHECKING: from typing import Any, Sequence __all__ = ["nnls"] COND = 1e-15 # Condition number cutoff for least squares solver initialization def _nnls_obj( x: np.ndarray, shape: Sequence[int], A: np.ndarray, B: np.ndarray ) -> tuple[float, np.ndarray]: """Compute the objective and gradient for NNLS""" # Scipy's lbfgs flattens all arrays, so we first reshape # the iterate x x = x.reshape(shape) # Compute the difference matrix diff = np.einsum("mf,...ft->...mt", A, x, optimize=True) - B # Compute the objective value value = (1 / B.size) * 0.5 * np.sum(diff**2) # And the gradient grad = (1 / B.size) * np.einsum("mf,...mt->...ft", A, diff, optimize=True) # Flatten the gradient return value, grad.flatten() def _nnls_lbfgs_block( A: np.ndarray, B: np.ndarray, x_init: np.ndarray | None = None, **kwargs: Any ) -> np.ndarray: """Solve the constrained problem over a single block Parameters ---------- A : np.ndarray [shape=(m, d)] The basis matrix B : np.ndarray [shape=(m, N)] The regression targets x_init : np.ndarray [shape=(d, N)] An initial guess **kwargs Additional keyword arguments to `scipy.optimize.fmin_l_bfgs_b` Returns ------- x : np.ndarray [shape=(d, N)] Non-negative matrix such that Ax ~= B """ # If we don't have an initial point, start at the projected # least squares solution import scipy.linalg import scipy.optimize if x_init is None: # Suppress type checks because mypy can't find pinv x_init = np.einsum("fm,...mt->...ft", scipy.linalg.pinv(A, rtol=COND), B, optimize=True) np.clip(x_init, 0, None, out=x_init) # Adapt the hessian approximation to the dimension of the problem kwargs.setdefault("m", A.shape[1]) # Construct non-negative bounds bounds = [(0, None)] * x_init.size shape = x_init.shape # optimize x, _obj_value, _diagnostics = scipy.optimize.fmin_l_bfgs_b( _nnls_obj, x_init, args=(shape, A, B), bounds=bounds, **kwargs ) # reshape the solution return x.reshape(shape) def nnls(A: np.ndarray, B: np.ndarray, **kwargs: Any) -> np.ndarray: """Non-negative least squares. Given two matrices A and B, find a non-negative matrix X that minimizes the sum squared error:: err(X) = sum_i,j ((AX)[i,j] - B[i, j])^2 Parameters ---------- A : np.ndarray [shape=(m, n)] The basis matrix B : np.ndarray [shape=(..., m, N)] The target array. Additional leading dimensions are supported. **kwargs Additional keyword arguments to `scipy.optimize.fmin_l_bfgs_b` Returns ------- X : np.ndarray [shape=(..., n, N), non-negative] A minimizing solution to ``|AX - B|^2`` See Also -------- scipy.optimize.nnls scipy.optimize.fmin_l_bfgs_b Examples -------- Approximate a magnitude spectrum from its mel spectrogram >>> y, sr = librosa.loadx('trumpet', duration=3) >>> S = np.abs(librosa.stft(y, n_fft=2048)) >>> M = librosa.feature.melspectrogram(S=S, sr=sr, power=1) >>> mel_basis = librosa.filters.mel(sr=sr, n_fft=2048, n_mels=M.shape[0]) >>> S_recover = librosa.util.nnls(mel_basis, M) Plot the results >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=3, sharex=True, sharey=True) >>> librosa.display.specshow(S, vscale='dBFS', ... y_axis='log', x_axis='time', ax=ax[2]) >>> ax[2].set(title='Original spectrogram (1025 bins)') >>> ax[2].label_outer() >>> librosa.display.specshow(M, vscale='dBFS', ... y_axis='mel', x_axis='time', ax=ax[0]) >>> ax[0].set(title='Mel spectrogram (128 bins)') >>> ax[0].label_outer() >>> img = librosa.display.specshow(S_recover, vscale=f'dB[{S.max()}]', ... y_axis='log', x_axis='time', ax=ax[1]) >>> ax[1].set(title='Reconstructed spectrogram (1025 bins)') >>> ax[1].label_outer() >>> librosa.display.colorbar_db(img, ax=ax) """ import scipy.optimize # If B is a single vector, punt up to the scipy method if B.ndim == 1: return scipy.optimize.nnls(A, B)[0] n_columns = int(MAX_MEM_BLOCK // (np.prod(B.shape[:-1]) * A.itemsize)) n_columns = max(n_columns, 1) # Process in blocks: if B.shape[-1] <= n_columns: return _nnls_lbfgs_block(A, B, **kwargs).astype(A.dtype) x: np.ndarray x = np.einsum("fm,...mt->...ft", scipy.linalg.pinv(A, rtol=COND), B, optimize=True) np.clip(x, 0, None, out=x) x_init = x for bl_s in range(0, x.shape[-1], n_columns): bl_t = min(bl_s + n_columns, B.shape[-1]) x[..., bl_s:bl_t] = _nnls_lbfgs_block( A, B[..., bl_s:bl_t], x_init=x_init[..., bl_s:bl_t], **kwargs ) return x ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2139711 librosa-1.0.0/librosa/util/decorators.py0000644000175100017510000001432615236640653017760 0ustar00runnerrunner#!/usr/bin/env python # -*- encoding: utf-8 -*- # CREATED:2015-02-15 10:06:03 by Brian McFee """Helpful tools for deprecation""" from __future__ import annotations import functools import inspect import warnings from typing import TYPE_CHECKING import numpy as np from decorator import decorator if TYPE_CHECKING: from collections.abc import Callable, Iterable from typing import Any, Protocol, type_check_only from numpy.typing import DTypeLike @type_check_only class _Decorator(Protocol): def __call__[**P, R](self, fn: Callable[P, R], /) -> Callable[P, R]: ... __all__ = ["moved", "deprecated", "vectorize"] def moved(*, moved_from: str, version: str, version_removed: str) -> _Decorator: """Mark functions as moved/renamed. Using the decorated (old) function will result in a warning. Parameters ---------- moved_from : str The old location of the function version : str The version in which the function was moved version_removed : str The version in which the old alias will be removed Returns ------- decorator : Callable A decorator that can be applied to the old function name """ def __wrapper[**P, R](func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> R: """Warn the user, and then proceed.""" warnings.warn( "{:s}\n\tThis function was moved to '{:s}.{:s}' in " "librosa version {:s}." "\n\tThis alias will be removed in librosa version " "{:s}.".format( moved_from, func.__module__, func.__name__, version, version_removed ), category=FutureWarning, stacklevel=3, # Would be 2, but the decorator adds a level ) return func(*args, **kwargs) return decorator(__wrapper) def deprecated(*, version: str, version_removed: str) -> _Decorator: """Mark a function as deprecated. Using the decorated (old) function will result in a warning. Parameters ---------- version : str The version in which the function was deprecated version_removed : str The version in which the function will be removed Returns ------- decorator : Callable A decorator that can be applied to the deprecated function """ def __wrapper[**P, R](func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> R: """Warn the user, and then proceed.""" warnings.warn( "{:s}.{:s}\n\tDeprecated as of librosa version {:s}." "\n\tIt will be removed in librosa version {:s}.".format( func.__module__, func.__name__, version, version_removed ), category=FutureWarning, stacklevel=3, # Would be 2, but the decorator adds a level ) return func(*args, **kwargs) return decorator(__wrapper) def vectorize( *, otypes: str | Iterable[DTypeLike] | None = None, doc: str | None = None, excluded: Iterable[int | str] | None = None, cache: bool = False, signature: str | None = None, ) -> _Decorator: """Wrap a function for use with np.vectorize. This function is not quite a decorator, but is used as a wrapper to np.vectorize that preserves scalar behavior. Parameters ---------- otypes : str or list of dtype, optional The output data type(s). This is required if the function returns a scalar. If the function returns an array, this is optional. doc : str, optional The docstring for the vectorized function. If None, the original function's docstring will be used. excluded : list of int or str, optional List of argument indices or names to exclude from vectorization. cache : bool, optional If True, cache the results of the function calls. This can speed up repeated calls with the same arguments, but may consume more memory. signature : str, optional The signature of the function for generalized ufunc behavior. Returns ------- decorator : Callable A decorator that can be applied to the function to vectorize it. See Also -------- np.vectorize : The underlying vectorization function from NumPy. """ def __wrapper(function): vecfunc = np.vectorize( function, otypes=otypes, doc=doc, excluded=excluded, cache=cache, signature=signature, ) @functools.wraps(function) def _vec(*args, **kwargs): y = vecfunc(*args, **kwargs) if np.isscalar(args[0]): return y.item() else: return y return _vec return __wrapper def future_default(*, param_name: str, old_default: Any, new_default: Any, version: str) -> _Decorator: """Mark a function parameter as having a future change to a default parameter value. Parameters ---------- param_name : str The name of the parameter whose default value is changing. old_default : Any The current default value of the parameter. new_default : Any The future default value of the parameter. version : str The version in which the default value will change. Returns ------- decorator : Callable A decorator that can be applied to the function to warn about the future change in default value """ def decorator[**P, R](func: Callable[P, R]) -> Callable[P, R]: sig = inspect.signature(func) @functools.wraps(func) def __wrapper(*args: P.args, **kwargs: P.kwargs) -> R: # functools.wraps does not inject defaults into args/kwargs bound = sig.bind(*args, **kwargs) if param_name not in bound.arguments: warnings.warn( f"The default value of '{param_name}' will change from " f"{old_default!r} to {new_default!r} in librosa version {version}. " f"To suppress this warning, explicitly pass '{param_name}={old_default!r}'.", FutureWarning, stacklevel=2 ) return func(*args, **kwargs) return __wrapper return decorator ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2139711 librosa-1.0.0/librosa/util/deprecation.py0000644000175100017510000000333115236640653020102 0ustar00runnerrunner#!/usr/bin/env python # -*- encoding: utf-8 -*- """Deprecation utilities""" import inspect import warnings from typing import Any class Deprecated: """A placeholder class to catch usage of deprecated variable names""" def __repr__(self) -> str: """Pretty-print display for deprecated objects""" return "" def rename_kw( *, old_name: str, old_value: Any, new_name: str, new_value: Any, version_deprecated: str, version_removed: str ) -> Any: """Handle renamed arguments. Parameters ---------- old_name : str The name of the old argument old_value : Any The value of the old argument new_name : str The name of the new argument new_value : Any The value of the new argument version_deprecated : str The version at which the old name became deprecated version_removed : str The version at which the old name will be removed Returns ------- value : Any - ``new_value`` if ``old_value`` of type `Deprecated` - ``old_value`` otherwise Warnings -------- if ``old_value`` is not of type `Deprecated` """ if isinstance(old_value, Deprecated): return new_value else: stack = inspect.stack() dep_func = stack[1] warnings.warn( "{:s}() keyword argument '{:s}' has been " "renamed to '{:s}' in version {:}." "\n\tThis alias will be removed in version " "{:}.".format( dep_func[3], old_name, new_name, version_deprecated, version_removed ), category=FutureWarning, stacklevel=3, ) return old_value ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462640.9857752 librosa-1.0.0/librosa/util/example_data/0000755000175100017510000000000015236640661017656 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2139711 librosa-1.0.0/librosa/util/example_data/__init__.py0000644000175100017510000000005115236640653021764 0ustar00runnerrunner"""Resources for loading example data""" ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1786462635.214252 librosa-1.0.0/librosa/util/example_data/index.json0000644000175100017510000000445215236640653021666 0ustar00runnerrunner{ "vibeace": { "path": "Kevin_MacLeod_-_Vibe_Ace", "desc": "Kevin MacLeod - Vibe Ace" }, "choice": { "path": "admiralbob77_-_Choice_-_Drum-bass", "desc": "Admiral Bob - Choice (drum+bass)" }, "nutcracker": { "path": "Kevin_MacLeod_-_P_I_Tchaikovsky_Dance_of_the_Sugar_Plum_Fairy", "desc": "Tchaikovsky - Dance of the Sugar Plum Fairy" }, "brahms": { "path": "Hungarian_Dance_number_5_-_Allegro_in_F_sharp_minor_(string_orchestra)", "desc": "Brahms - Hungarian Dance #5" }, "trumpet": { "path": "sorohanro_-_solo-trumpet-06", "desc": "Mihai Sorohan - Trumpet loop" }, "fishin": { "path": "Karissa_Hobbs_-_Lets_Go_Fishin", "desc": "Karissa Hobbs - Let's Go Fishin'" }, "sweetwaltz": { "path": "147793__setuniman__sweet-waltz-0i-22mi", "desc": "Setuniman - Sweet Waltz" }, "humpback": { "path": "glacier-bay-humpback", "desc": "Glacier Bay 60-second clip humpback whale song November 2020" }, "libri1": { "path": "5703-47212-0000", "desc": "The Ashiel Mystery: A Detective Story, chapter XVI. / Read by Garth Comira" }, "libri2": { "path": "3436-172162-0000", "desc": "The Age of Chivalry, chapter IX: The Adventure of the Cart / Read by Anders Lankford" }, "pistachio": { "path": "442789__lena-orsa__happy-music-pistachio-ice-cream-ragtime", "desc": "The Piano Lady - Pistachio Ice Cream Ragtime" }, "robin": { "path": "456440__inspectorj__bird-whistling-robin-single-13", "desc": "Bird Whistling, Robin, Single, 13.wav / InspectorJ" }, "libri3": { "path": "198-209-0000", "desc": "Sense and Sensibility / Chapter 18 / Jane Austen / Read by Heather Barnett" }, "pibble": { "path": "pibble", "desc": "Who's a good girl?" }, "snare": { "path": "snare-accelerate", "desc": "An accelerating beat on a snare drum" }, "drese": { "path": "drese-midi", "desc": "Multi-track recording of five instruments playing in unison" }, "drese2": { "path": "drese-tempo", "desc": "Alternate recording of \"drese\" example on trumpet with tempo variation" } } ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1786462635.214252 librosa-1.0.0/librosa/util/example_data/registry.txt0000644000175100017510000001634615236640653022302 0ustar00runnerrunner147793__setuniman__sweet-waltz-0i-22mi.hq.ogg 248f93b7c729bc0bd6c4cbc107e6b67ec4d60e40c111e313d4d69f77748da26a 147793__setuniman__sweet-waltz-0i-22mi.ogg 4baff8ebf1771c33618b58aa12ac3fbac1e0462894ae74247f2fb8e649d1c63b 147793__setuniman__sweet-waltz-0i-22mi.toml 4fb8b53f390e5926d58ab3687db09f2e58aa01820de14ffe739ac230b3b722ae 147793__setuniman__sweet-waltz-0i-22mi.txt 6e6e9265ae9a9a36033ce305a893abdc15f43bb5a41ec29efba368ad91905f43 198-209-0000.hq.ogg 6a8d2c16e56dcb27b7f5fe5aa99bfd26b4722a6dabca1968eff951b456936514 198-209-0000.ogg 0473ba9613befa1026db5e254546f105aeffba8366b5468fc5e8d474d844ca1d 198-209-0000.toml b41a90dbe8d7ca36dc7498fef5524b95dd8dc9e8c8bef6110f207afed8b57b46 198-209-0000.txt c5bf58b2a4aa0d6bf1e6051690312f3238960d455776a6d37e469d80db1da167 3436-172162-0000.hq.ogg 8e034529f2f171c7e5e772df6eccc2d169183c39605b45b351c2bfabddf14162 3436-172162-0000.ogg 780e482b52f2c8a500356babf60178ce005449cc171a8537eb3ef2961eb3e855 3436-172162-0000.toml f9b76b7a994db493f76fe2aacd5615d6b35f49b5674b87066ccb10daaadba08f 3436-172162-0000.txt 8cf4b20e148c4366205bc607f447c169256e945d62c64ead43d0a4dfa7d3ab98 442789__lena-orsa__happy-music-pistachio-ice-cream-ragtime.hq.ogg 1cc83b775e640a8dd409da9e29f5b5e0124bc5ec9bb4fda975919d31d1d893b8 442789__lena-orsa__happy-music-pistachio-ice-cream-ragtime.ogg 9617c9be55c128177b13c20fbc52178ed482e3545094517efb30a7db2798991e 442789__lena-orsa__happy-music-pistachio-ice-cream-ragtime.toml cf86318441db95cb164c0ae5604f6a4a0d19b647915c36278d5bb0cf3713eb30 442789__lena-orsa__happy-music-pistachio-ice-cream-ragtime.txt 99f9c44368918572ac154ae4a2fec4020d1d4bb418ae7bf241864916d68f9d04 456440__inspectorj__bird-whistling-robin-single-13.hq.ogg a3b3ecf749befde43bdf35f839fdcb8d399a4deb5666de6f399c35ce12936baa 456440__inspectorj__bird-whistling-robin-single-13.ogg 57c2b861d028e25d7c086b48853f20eda1ae9a7a33e1125a1f3bec91539b4208 456440__inspectorj__bird-whistling-robin-single-13.toml 02ea9822a157c4953b331bb02c238ec580a5cb297c976d650f625cfef934feac 456440__inspectorj__bird-whistling-robin-single-13.txt 34d25a379408d9144fbc4ce8cceb72be5196aeb6140cec6c90e6bdcb0567cb6e 5703-47212-0000.hq.ogg f09254a0daf4b14b292868d46dc2e3c8e158d19fafff739ad4c3931e2ce7b1b0 5703-47212-0000.ogg a284612b46af0535f7e1873758c4387bb8369f6dbbe192ffdec1f171108f98dd 5703-47212-0000.toml 0d3f9a04b764d9474a93959633b560ae3ab65163123773f58c98a21908b67901 5703-47212-0000.txt 1c5bd5eb792ff0d2fbe895e3a3dd0acf3beb48ee364e64e12cde3a646304a9f4 Hungarian_Dance_number_5_-_Allegro_in_F_sharp_minor_(string_orchestra).hq.ogg 8e93ff0182a93168b15346c497b164cb49d2a97bf1e987a1149ea579e914532e Hungarian_Dance_number_5_-_Allegro_in_F_sharp_minor_(string_orchestra).ogg 919b48aa4cc66a0357d2cd5728664c5ab8f15c4b3469460df4b59470d35d3e49 Hungarian_Dance_number_5_-_Allegro_in_F_sharp_minor_(string_orchestra).toml c5c1edbb02204a228ac21741238f2556512eed9d0218d7d80d2bc054642997b7 Hungarian_Dance_number_5_-_Allegro_in_F_sharp_minor_(string_orchestra).txt 0c857ef8a7365e3df9d5c25c2b08ec3169c0585e3ec08c9c6423d2a04d72fe5f Karissa_Hobbs_-_Let's_Go_Fishin'.hq.ogg 85901bde0bc5f2cad28acb83487ccf99bd6908d7359f1a449fd799d8bebb3319 Karissa_Hobbs_-_Let's_Go_Fishin'.ogg 27b3667c396c1831511aa3c415fcf582b6e8be560cafb844c5b67b76b72c1cb3 Karissa_Hobbs_-_Let's_Go_Fishin'.toml 5d1cfee2c06789d542526629b2520097538bd8b6fc21e692330a03c1f182c159 Karissa_Hobbs_-_Let's_Go_Fishin'.txt 199bf3408b98916cd9d28a22b2b43c1935ab70072b46fa05c0b9f40e7882802e Karissa_Hobbs_-_Lets_Go_Fishin.hq.ogg 85901bde0bc5f2cad28acb83487ccf99bd6908d7359f1a449fd799d8bebb3319 Karissa_Hobbs_-_Lets_Go_Fishin.ogg 27b3667c396c1831511aa3c415fcf582b6e8be560cafb844c5b67b76b72c1cb3 Karissa_Hobbs_-_Lets_Go_Fishin.toml 5d1cfee2c06789d542526629b2520097538bd8b6fc21e692330a03c1f182c159 Karissa_Hobbs_-_Lets_Go_Fishin.txt 199bf3408b98916cd9d28a22b2b43c1935ab70072b46fa05c0b9f40e7882802e Kevin_MacLeod_-_P_I_Tchaikovsky_Dance_of_the_Sugar_Plum_Fairy.hq.ogg f062221a56a227cdb7c067cf2e6ac0e250a50012f7693ca0c8e31f05f83e49b1 Kevin_MacLeod_-_P_I_Tchaikovsky_Dance_of_the_Sugar_Plum_Fairy.ogg b5c1a3e26310e6618d3c124f458654cd235650fcb9db7d711302644566600484 Kevin_MacLeod_-_P_I_Tchaikovsky_Dance_of_the_Sugar_Plum_Fairy.toml 70a5291888351a1ae97c22b53d681bc0e1bcbee57e60d6c857816ed4625d089e Kevin_MacLeod_-_P_I_Tchaikovsky_Dance_of_the_Sugar_Plum_Fairy.txt 059acb340170385d2bfa4c7ab7c2a06b1d8f8af3e0f11cb4f46ff4049e950915 Kevin_MacLeod_-_Vibe_Ace.hq.ogg 73d6443ef90a7c022f164e5aa90e56c2291585930b39b1656d0765abbc1f1779 Kevin_MacLeod_-_Vibe_Ace.ogg 6c23aed3dd5aa57f2b1652ecab68d15d9b82ad257f54e639eb2880ca09bc118a Kevin_MacLeod_-_Vibe_Ace.toml 6cab615af5610736079cafb267b6377432c6fec44bd2fc27fd6cc41b350e630e Kevin_MacLeod_-_Vibe_Ace.txt 6c71e0525cb0452ea74c6d6f5fde6fa1e221223db7b2aa35b9914b98367ee7b9 admiralbob77_-_Choice_-_Drum-bass.hq.ogg 57b4d95473b92a8441c1d3ab20f836a3e0cbf501bf3afe1ce7a5d0d98d7d4576 admiralbob77_-_Choice_-_Drum-bass.ogg ac644f9645e7c15174e4a4f8561e4d1448d7f6e59ff6b0556b310ebbced879bc admiralbob77_-_Choice_-_Drum-bass.toml 64350ac7b9b2108c891dd61a5d62f9987855f36fa6ed08477d0c291280d9672f admiralbob77_-_Choice_-_Drum-bass.txt 76525d6a4fd135053c5ff7463ae43a8ffcf064b575e8ef2fc3eb37786a45342b bib_index.msgpack 806a594d28681b6c834c63af4c94b1ea381d5323e2b681c16763d3dd2b17f0b9 drese-midi.hq.ogg 65eef9fa6eee0e3d5e3a0e78ab41e52e4ff312ed89505ed6b99b1ec7dd87b90b drese-midi.ogg e38dab530f50de8f79873fefbca3e0a5af454eb5906c06b84287d7c2e936dd19 drese-midi.toml e4de364187e991bf73a0bb1ee147ab05f91296a9050bdf952797d0072f175a37 drese-midi.txt cbb4bf423f2e63e366e3405985cfb97dcd653de608eb0a44cc71b7d49649288c drese-tempo.hq.ogg c248e4523125bafd8f38c4cc856e64ef764f55b25312bf641190245a66a1f089 drese-tempo.ogg bbaa7f8984266f289d7aaabfba4ba78f48f084cb0d1a03a6a84957f223b3030e drese-tempo.toml ab3eb902fc8197cc4d2c18a2ef3155794cdf71fbb711ae1900f13bc9bbe4ee9f drese-tempo.txt 8bc172e4519f9be2b4e7ce637c72777f052dce92e29ecbc04db8e4263e5d6a7e glacier-bay-humpback.hq.ogg c3250ff526898aa5528aeb6b81a49aff58f63400ec2ddb8bcb86caf611d33144 glacier-bay-humpback.ogg 64395c617b28b5e31a03032b99b4617bc96b926f3bc5c141862cd13fb79ccd8e glacier-bay-humpback.toml 71e52385433f11755ea5ce4120deb25fd4b9b1c687a43f18b9ee440ded5cad8d glacier-bay-humpback.txt bec2c73fe2368604f161ffdd44b7afea5f597473e2d51c03906bb05c22113910 pibble.hq.ogg dfd796becf38b6f5ff606f2c822ce24e81d8df936b1ced9bffd29e0d1f05b800 pibble.ogg c596c027f604defa522a1734f599c6db5624fa62dd3f1be2a933d85b077f7400 pibble.toml 7486041917f587abcdcf4d867a3badd21503b3ffad9490e3a31dfa2866192b1a pibble.txt 341fdf46cae7c165875667ed729f6d18983e8cfe32e491816705e7be00d57c76 snare-accelerate.hq.ogg c4b237c784504cec7896e5c4b4e99e0774cf249129b627da94bbbea9e2c6605a snare-accelerate.ogg c4b237c784504cec7896e5c4b4e99e0774cf249129b627da94bbbea9e2c6605a snare-accelerate.toml 7f64890389d913a4a575887b4540b0842dd446f05fd240c7a991ad9843f11c7b snare-accelerate.txt de1d07a321c61283c008e32347f1694db92a4e91f0e3023df765cbdb254eb673 sorohanro_-_solo-trumpet-06.hq.ogg beb954ae2c9c16919b5ca6973d6d5196cdcb196b46a3c2a201dd8861e7e324de sorohanro_-_solo-trumpet-06.ogg 8374466fd3951d24509da6e799b132a0db0bdeda69d99c69d989a6888d3d727d sorohanro_-_solo-trumpet-06.toml c7c622d87ee7459b58bc5b8ceec7fa27aac95a625bbcca48d123aab79f2cb494 sorohanro_-_solo-trumpet-06.txt 750a191b9d0cc94b2f19cfbf11acc13783f75bedcccfcf562f5b076efd068aba version_index.msgpack 02c3e9ab82032ea6b7fe3763b751e458b8d286b0d76b41aadb546d852c5cdfe3 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1786462635.214252 librosa-1.0.0/librosa/util/exceptions.py0000644000175100017510000000041215236640653017763 0ustar00runnerrunner#!/usr/bin/env python # -*- encoding: utf-8 -*- """Exception classes for librosa""" class LibrosaError(Exception): """The root librosa exception class""" pass class ParameterError(LibrosaError): """Exception class for mal-formed inputs""" pass ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1786462635.214252 librosa-1.0.0/librosa/util/files.py0000644000175100017510000003174215236640653016716 0ustar00runnerrunner#!/usr/bin/env python # -*- coding: utf-8 -*- """Utility functions for dealing with files""" from __future__ import annotations import contextlib import glob import html as html_lib import json import os from importlib import resources from typing import Any import msgpack import pooch import tomllib from ..version import version as librosa_version from .exceptions import ParameterError __all__ = [ "find_files", "example", "ex", "list_examples", "example_info", ] # Instantiate the pooch __data_path = os.environ.get("LIBROSA_DATA_DIR", pooch.os_cache("librosa")) __GOODBOY = pooch.create( __data_path, base_url="https://librosa.org/data/audio/", registry=None ) @contextlib.contextmanager def _resource_file(package: str, resource: str): """Provide a context manager for accessing resources in a package. This previously served as a shim wrapper to support Python < 3.9, but now it's a convenience function to simplify path mangling. """ with resources.as_file(resources.files(package).joinpath(resource)) as path: yield path with _resource_file("librosa.util.example_data", "registry.txt") as reg: __GOODBOY.load_registry(str(reg)) # We want to bypass version checks here to allow asynchronous updates for new releases __GOODBOY.registry["version_index.msgpack"] = None __GOODBOY.registry["bib_index.msgpack"] = None with _resource_file("librosa.util.example_data", "index.json") as index, index.open("r") as _fdesc: __TRACKMAP = json.load(_fdesc) def example(key: str, *, hq: bool = False, url: bool = False) -> str: """Retrieve the example recording identified by 'key'. The first time an example is requested, it will be downloaded from the remote repository over HTTPS. All subsequent requests will use a locally cached copy of the recording. For a list of examples (and their keys), see `librosa.util.list_examples`. By default, local files will be cached in the directory given by `pooch.os_cache('librosa')`. You can override this by setting an environment variable ``LIBROSA_DATA_DIR`` prior to importing librosa: >>> import os >>> os.environ['LIBROSA_DATA_DIR'] = '/path/to/store/data' >>> import librosa Parameters ---------- key : str The identifier for the track to load hq : bool If ``True``, return the high-quality version of the recording. If ``False``, return the 22KHz mono version of the recording. url : bool If ``True``, return the URL to the example file instead of the local path. If ``False`` (default), return the local path to the example file. Returns ------- path : str The path or remote URL to the requested example file Examples -------- Load "Hungarian Dance #5" by Johannes Brahms >>> y, sr = librosa.load(librosa.example('brahms')) Load "Vibe Ace" by Kevin MacLeod (the example previously packaged with librosa) in high-quality mode >>> y, sr = librosa.load(librosa.example('vibeace', hq=True)) See Also -------- librosa.util.list_examples pooch.os_cache """ if key not in __TRACKMAP: raise ParameterError(f"Unknown example key: {key}") if hq: ext = ".hq.ogg" else: ext = ".ogg" remote_url = __TRACKMAP[key]["path"] + ext if url: return str(__GOODBOY.base_url + remote_url) else: return str(__GOODBOY.fetch(remote_url)) ex = example """Alias for example""" def list_examples() -> None: """List the available audio recordings included with librosa. Each recording is given a unique identifier (e.g., "brahms" or "nutcracker"), listed in the first column of the output. A brief description is provided in the second column. See Also -------- util.example util.example_info """ print("AVAILABLE EXAMPLES") print("-" * 68) for key in sorted(__TRACKMAP.keys()): if key == "pibble": # Shh... she's sleeping continue print(f"{key:10}\t{__TRACKMAP[key]['desc']}") def _resolve_license_url(spdx_id: str) -> str | None: """Dynamically map SPDX identifiers to canonical Creative Commons URLs.""" spdx_id = spdx_id.upper() if spdx_id == "CC0-1.0": return "https://creativecommons.org/publicdomain/zero/1.0/" if spdx_id == "CC-PDM-1.0": return "https://creativecommons.org/publicdomain/mark/1.0/" if spdx_id.startswith("CC-"): parts = spdx_id.split("-")[1:] if len(parts) >= 2: version = parts[-1] variant = "-".join(parts[:-1]).lower() return f"https://creativecommons.org/licenses/{variant}/{version}/" return None def example_info(key: str, *, html: bool = False) -> str | None: """Display licensing and metadata information for the given example recording. The first time an example is requested, it will be downloaded from the remote repository over HTTPS. All subsequent requests will use a locally cached copy of the recording. For a list of examples (and their keys), see `librosa.util.list_examples`. By default, local files will be cached in the directory given by `pooch.os_cache('librosa')`. You can override this by setting an environment variable ``LIBROSA_DATA_DIR`` prior to importing librosa. Parameters ---------- key : str The identifier for the recording (see `list_examples`) html : bool If ``True``, return the information in HTML format. If ``False``, print the information in plain text format. Returns ------- info : str or None If `html` is ``True``, return the HTML formatted string. Otherwise, no return value. See Also -------- librosa.util.example librosa.util.list_examples pooch.os_cache """ if key not in __TRACKMAP: raise ParameterError(f"Unknown example key: {key}") license_file = __GOODBOY.fetch(__TRACKMAP[key]["path"] + ".toml") with open(license_file, "rb") as fdesc: data = tomllib.load(fdesc) title = data["title"] attribution = data["attribution"] description = data["description"] spdx_id = data["license"] corpus = data.get("corpus", None) url = data.get("url", None) if html: title_h = html_lib.escape(title) attr_h = html_lib.escape(attribution) desc_h = html_lib.escape(description).replace("\n", "
") spdx_h = html_lib.escape(spdx_id) corpus_h = "" if corpus: corpus_h = f"
Corpus: {html_lib.escape(corpus)}
" url_h = "" if url: url_h = f'' license_url = _resolve_license_url(spdx_h) license_html = f'{spdx_h}' if license_url else spdx_h return f"""
{title_h}
By: {attr_h} | License: {license_html}{url_h}
{corpus_h}
Description

{desc_h}

""" else: lines = [ title, "=" * len(title), f"Attribution: {attribution}" ] if corpus: lines.append(f"Corpus: {corpus}") lines.append(f"License: {spdx_id}") if url: lines.append(f"Source: {url}") lines.extend(["", description.strip()]) print("\n".join(lines)) return None def find_files( directory: str | os.PathLike[Any], *, ext: str | list[str] | None = None, recurse: bool = True, case_sensitive: bool = False, limit: int | None = None, offset: int = 0, ) -> list[str]: """Get a sorted list of (audio) files in a directory or directory sub-tree. Examples -------- >>> # Get all audio files in a directory sub-tree >>> files = librosa.util.find_files('~/Music') >>> # Look only within a specific directory, not the sub-tree >>> files = librosa.util.find_files('~/Music', recurse=False) >>> # Only look for mp3 files >>> files = librosa.util.find_files('~/Music', ext='mp3') >>> # Or just mp3 and ogg >>> files = librosa.util.find_files('~/Music', ext=['mp3', 'ogg']) >>> # Only get the first 10 files >>> files = librosa.util.find_files('~/Music', limit=10) >>> # Or last 10 files >>> files = librosa.util.find_files('~/Music', offset=-10) >>> # Avoid including search patterns in the path string >>> import glob >>> directory = '~/[202206] Music' >>> directory = glob.escape(directory) # Escape the special characters >>> files = librosa.util.find_files(directory) Parameters ---------- directory : str Path to look for files ext : str or list of str A file extension or list of file extensions to include in the search. Default: ``['aac', 'au', 'flac', 'm4a', 'mp3', 'ogg', 'wav']`` recurse : bool If ``True``, then all subfolders of ``directory`` will be searched. Otherwise, only ``directory`` will be searched. case_sensitive : bool If ``False``, files matching upper-case version of extensions will be included. limit : int > 0 or None Return at most ``limit`` files. If ``None``, all files are returned. offset : int Return files starting at ``offset`` within the list. Use negative values to offset from the end of the list. Returns ------- files : list of str The list of audio files. """ if ext is None: ext = ["aac", "au", "flac", "m4a", "mp3", "ogg", "wav"] elif isinstance(ext, str): ext = [ext] # Cast into a set ext = set(ext) # Generate upper-case versions if not case_sensitive: # Force to lower-case ext = {e.lower() for e in ext} # Add in upper-case versions ext |= {e.upper() for e in ext} fileset = set() if recurse: for walk in os.walk(directory): # type: ignore fileset |= __get_files(walk[0], ext) else: fileset = __get_files(directory, ext) files = list(fileset) files.sort() files = files[offset:] if limit is not None: files = files[:limit] return files def __get_files(dir_name: str | os.PathLike[Any], extensions: set[str]): """Get a list of files in a single directory""" # Expand out the directory dir_name = os.path.abspath(os.path.expanduser(dir_name)) myfiles = set() for sub_ext in extensions: globstr = os.path.join(dir_name, "*" + os.path.extsep + sub_ext) myfiles |= set(glob.glob(globstr)) return myfiles def cite(version: str | None=None, *, bib: bool = False) -> str: r"""Print the citation information for librosa. Parameters ---------- version : str or None The version of librosa to cite. If None, the current version is used. bib : bool If True, return the BibTeX entry for the given version of librosa. If False (default), return the DOI for the given version of librosa. Returns ------- doi : str The DOI for the given version of librosa. Raises ------ ParameterError If the requested version is not found in the citation index. Examples -------- >>> librosa.cite("0.10.1") "https://doi.org/10.5281/zenodo.8252662" >>> bib = librosa.cite("0.11.0", bib=True) >>> print(bib[:140], "...") @misc{https://doi.org/10.5281/zenodo.15006942, doi = {10.5281/ZENODO.15006942}, url = {https://zenodo.org/doi/10.5281/zenodo.15006942}, ... """ if version is None: version = librosa_version version_data = __GOODBOY.fetch("version_index.msgpack") with open(version_data, "rb") as fdesc: version_index: dict[str, str] = msgpack.load(fdesc) if version not in version_index: if "dev" in version: raise ParameterError(f"Version {version} is not yet released and " "therefore does not yet have a citable DOI.") else: raise ParameterError(f"Version {version} not found in the citation index") if bib: bib_data = __GOODBOY.fetch("bib_index.msgpack") with open(bib_data, "rb") as fdesc: bib_index: dict[str, str] = msgpack.load(fdesc) try: return bib_index[version] except KeyError as exc: raise ParameterError(f"No BibTeX entry found for version {version}.") from exc return f"https://doi.org/{version_index[version]}" ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1786462635.214252 librosa-1.0.0/librosa/util/matching.py0000644000175100017510000003055515236640653017407 0ustar00runnerrunner#!/usr/bin/env python # -*- coding: utf-8 -*- """Matching functions""" from __future__ import annotations from typing import TYPE_CHECKING import numba import numpy as np from .exceptions import ParameterError from .utils import valid_intervals if TYPE_CHECKING: from .._typing import _Array1D, _SequenceLike __all__ = ["match_intervals", "match_events"] @numba.jit(nopython=True, cache=True) # type: ignore def __jaccard(int_a: np.ndarray, int_b: np.ndarray): # pragma: no cover """Jaccard similarity between two intervals Parameters ---------- int_a, int_b : np.ndarrays, shape=(2,) Returns ------- Jaccard similarity between intervals """ ends = [int_a[1], int_b[1]] if ends[1] < ends[0]: ends.reverse() starts = [int_a[0], int_b[0]] if starts[1] < starts[0]: starts.reverse() intersection = ends[0] - starts[1] if intersection < 0: intersection = 0.0 union = ends[1] - starts[0] if union > 0: return intersection / union return 0.0 @numba.jit(nopython=True, cache=True) def __match_interval_overlaps(query, intervals_to, candidates): # pragma: no cover """Find the best Jaccard match from query to candidates""" best_score = -1 best_idx = -1 for idx in candidates: score = __jaccard(query, intervals_to[idx]) if score > best_score: best_score, best_idx = score, idx return best_idx @numba.jit(nopython=True, cache=True) # type: ignore def __match_intervals( intervals_from: np.ndarray, intervals_to: np.ndarray, strict: bool = True ) -> _Array1D[np.uint32]: # pragma: no cover """Numba-accelerated interval matching algorithm.""" # sort index of the interval starts start_index = np.argsort(intervals_to[:, 0]) # sort index of the interval ends end_index = np.argsort(intervals_to[:, 1]) # and sorted values of starts start_sorted = intervals_to[start_index, 0] # and ends end_sorted = intervals_to[end_index, 1] search_ends = np.searchsorted(start_sorted, intervals_from[:, 1], side="right") search_starts = np.searchsorted(end_sorted, intervals_from[:, 0], side="left") output = np.empty(len(intervals_from), dtype=numba.uint32) for i in range(len(intervals_from)): query = intervals_from[i] # Find the intervals that start after our query ends after_query = search_ends[i] # And the intervals that end after our query begins before_query = search_starts[i] # Candidates for overlapping have to (end after we start) and (begin before we end) candidates = set(start_index[:after_query]) & set(end_index[before_query:]) # Proceed as before if len(candidates) > 0: output[i] = __match_interval_overlaps(query, intervals_to, candidates) elif strict: # Numba only lets us use compile-time constants in exception messages raise ParameterError else: # Find the closest interval # (start_index[after_query] - query[1]) is the distance to the next interval # (query[0] - end_index[before_query]) dist_before = np.inf dist_after = np.inf if search_starts[i] > 0: dist_before = query[0] - end_sorted[search_starts[i] - 1] if search_ends[i] + 1 < len(intervals_to): dist_after = start_sorted[search_ends[i] + 1] - query[1] if dist_before < dist_after: output[i] = end_index[search_starts[i] - 1] else: output[i] = start_index[search_ends[i] + 1] return output def match_intervals( intervals_from: np.ndarray, intervals_to: np.ndarray, strict: bool = True ) -> _Array1D[np.uint32]: """Match one set of time intervals to another. This can be useful for tasks such as mapping beat timings to segments. Each element ``[a, b]`` of ``intervals_from`` is matched to the element ``[c, d]`` of ``intervals_to`` which maximizes the Jaccard similarity between the intervals:: max(0, |min(b, d) - max(a, c)|) / |max(d, b) - min(a, c)| In ``strict=True`` mode, if there is no interval with positive intersection with ``[a,b]``, an exception is thrown. In ``strict=False`` mode, any interval ``[a, b]`` that has no intersection with any element of ``intervals_to`` is instead matched to the interval ``[c, d]`` which minimizes:: min(|b - c|, |a - d|) that is, the disjoint interval [c, d] with a boundary closest to [a, b]. .. note:: An element of ``intervals_to`` may be matched to multiple entries of ``intervals_from``. Parameters ---------- intervals_from : np.ndarray [shape=(n, 2)] The time range for source intervals. The ``i`` th interval spans time ``intervals_from[i, 0]`` to ``intervals_from[i, 1]``. ``intervals_from[0, 0]`` should be 0, ``intervals_from[-1, 1]`` should be the track duration. intervals_to : np.ndarray [shape=(m, 2)] Analogous to ``intervals_from``. strict : bool If ``True``, intervals can only match if they intersect. If ``False``, disjoint intervals can match. Returns ------- interval_mapping : np.ndarray [shape=(n,)] For each interval in ``intervals_from``, the corresponding interval in ``intervals_to``. See Also -------- match_events Raises ------ ParameterError If either array of input intervals is not the correct shape If ``strict=True`` and some element of ``intervals_from`` is disjoint from every element of ``intervals_to``. Examples -------- >>> ints_from = np.array([[3, 5], [1, 4], [4, 5]]) >>> ints_to = np.array([[0, 2], [1, 3], [4, 5], [6, 7]]) >>> librosa.util.match_intervals(ints_from, ints_to) array([2, 1, 2], dtype=uint32) >>> # [3, 5] => [4, 5] (ints_to[2]) >>> # [1, 4] => [1, 3] (ints_to[1]) >>> # [4, 5] => [4, 5] (ints_to[2]) The reverse matching of the above is not possible in ``strict`` mode because ``[6, 7]`` is disjoint from all intervals in ``ints_from``. With ``strict=False``, we get the following: >>> librosa.util.match_intervals(ints_to, ints_from, strict=False) array([1, 1, 2, 2], dtype=uint32) >>> # [0, 2] => [1, 4] (ints_from[1]) >>> # [1, 3] => [1, 4] (ints_from[1]) >>> # [4, 5] => [4, 5] (ints_from[2]) >>> # [6, 7] => [4, 5] (ints_from[2]) """ if len(intervals_from) == 0 or len(intervals_to) == 0: raise ParameterError("Attempting to match empty interval list") # Verify that the input intervals has correct shape and size valid_intervals(intervals_from) valid_intervals(intervals_to) try: # Suppress type check because of numba wrapper return __match_intervals(intervals_from, intervals_to, strict=strict) # type: ignore except ParameterError as exc: raise ParameterError(f"Unable to match intervals with strict={strict}") from exc def match_events( events_from: _SequenceLike, events_to: _SequenceLike, left: bool = True, right: bool = True, ) -> np.ndarray: """Match one set of events to another. This is useful for tasks such as matching beats to the nearest detected onsets, or frame-aligned events to the nearest zero-crossing. .. note:: A target event may be matched to multiple source events. Examples -------- >>> # Sources are multiples of 7 >>> s_from = np.arange(0, 100, 7) >>> s_from array([ 0, 7, 14, 21, 28, 35, 42, 49, 56, 63, 70, 77, 84, 91, 98]) >>> # Targets are multiples of 10 >>> s_to = np.arange(0, 100, 10) >>> s_to array([ 0, 10, 20, 30, 40, 50, 60, 70, 80, 90]) >>> # Find the matching >>> idx = librosa.util.match_events(s_from, s_to) >>> idx array([0, 1, 1, 2, 3, 3, 4, 5, 6, 6, 7, 8, 8, 9, 9]) >>> # Print each source value to its matching target >>> zip(s_from, s_to[idx]) [(0, 0), (7, 10), (14, 10), (21, 20), (28, 30), (35, 30), (42, 40), (49, 50), (56, 60), (63, 60), (70, 70), (77, 80), (84, 80), (91, 90), (98, 90)] Parameters ---------- events_from : ndarray [shape=(n,)] Array of events (eg, times, sample or frame indices) to match from. events_to : ndarray [shape=(m,)] Array of events (eg, times, sample or frame indices) to match against. left : bool See `right` right : bool If ``False``, then matched events cannot be to the left (or right) of source events. Returns ------- event_mapping : np.ndarray [shape=(n,)] For each event in ``events_from``, the corresponding event index in ``events_to``:: event_mapping[i] == arg min |events_from[i] - events_to[:]| See Also -------- match_intervals Raises ------ ParameterError If either array of input events is not the correct shape """ if len(events_from) == 0 or len(events_to) == 0: raise ParameterError("Attempting to match empty event list") # If we can't match left or right, then only strict equivalence # counts as a match. if not (left or right) and not np.all(np.isin(events_from, events_to)): raise ParameterError( "Cannot match events with left=right=False " "and events_from is not contained " "in events_to" ) # If we can't match to the left, then there should be at least one # target event greater-equal to every source event if (not left) and max(events_to) < max(events_from): raise ParameterError( "Cannot match events with left=False " "and max(events_to) < max(events_from)" ) # If we can't match to the right, then there should be at least one # target event less-equal to every source event if (not right) and min(events_to) > min(events_from): raise ParameterError( "Cannot match events with right=False " "and min(events_to) > min(events_from)" ) # array of matched items output = np.empty_like(events_from, dtype=np.int32) # Suppress type check because of numba return __match_events_helper(output, events_from, events_to, left, right) # type: ignore @numba.jit(nopython=True, cache=True) # type: ignore def __match_events_helper( output: np.ndarray, events_from: np.ndarray, events_to: np.ndarray, left: bool = True, right: bool = True, ): # pragma: no cover # mock dictionary for events from_idx = np.argsort(events_from) sorted_from = events_from[from_idx] to_idx = np.argsort(events_to) sorted_to = events_to[to_idx] # find the matching indices matching_indices = np.searchsorted(sorted_to, sorted_from) # iterate over indices in matching_indices for ind, middle_ind in enumerate(matching_indices): left_flag = False right_flag = False left_ind = -1 right_ind = len(matching_indices) left_diff = 0 right_diff = 0 mid_diff = 0 middle_ind = matching_indices[ind] sorted_from_num = sorted_from[ind] # Prevent oob from chosen index if middle_ind == len(sorted_to): middle_ind -= 1 # Permitted to look to the left if left and middle_ind > 0: left_ind = middle_ind - 1 left_flag = True # Permitted to look to right if right and middle_ind < len(sorted_to) - 1: right_ind = middle_ind + 1 right_flag = True mid_diff = abs(sorted_to[middle_ind] - sorted_from_num) if left and left_flag: left_diff = abs(sorted_to[left_ind] - sorted_from_num) if right and right_flag: right_diff = abs(sorted_to[right_ind] - sorted_from_num) if left_flag and ( (not right and (sorted_to[middle_ind] > sorted_from_num)) or (not right_flag and left_diff < mid_diff) or (left_diff < right_diff and left_diff < mid_diff) ): output[ind] = to_idx[left_ind] # Check if right should be chosen elif right_flag and (right_diff < mid_diff): output[ind] = to_idx[right_ind] # Selected index wins else: output[ind] = to_idx[middle_ind] # Undo sorting solutions = np.empty_like(output) solutions[from_idx] = output return solutions ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1786462635.214252 librosa-1.0.0/librosa/util/utils.py0000644000175100017510000024726015236640653016760 0ustar00runnerrunner#!/usr/bin/env python # -*- coding: utf-8 -*- """Utility functions""" from __future__ import annotations import itertools from typing import TYPE_CHECKING, cast, overload import numba import numpy as np import scipy.sparse from numpy.lib.stride_tricks import as_strided from .._cache import cache from .exceptions import ParameterError if TYPE_CHECKING: from typing import Any, Callable, Literal, Sequence from numpy.typing import DTypeLike, NDArray from .._typing import ( _Array1D, _Array2D, _Complex, _ComplexLike_co, _FloatLike_co, _InterpKind, _Number, _Real, _SequenceLike, _SparseArray, _SparseMatrix, ) type _PeakPickMethod = Literal["greedy", "dp_count", "dp_value"] # Constrain STFT block sizes to 256 KB MAX_MEM_BLOCK = 2**8 * 2**10 __all__ = [ "MAX_MEM_BLOCK", "frame", "pad_center", "expand_to", "fix_length", "valid_audio", "valid_int", "is_positive_int", "valid_intervals", "fix_frames", "axis_sort", "localmax", "localmin", "normalize", "peak_pick", "sparsify_rows", "shear", "stack", "fill_off_diagonal", "index_to_slice", "sync", "softmask", "buf_to_float", "tiny", "cyclic_gradient", "dtype_r2c", "dtype_c2r", "count_unique", "is_unique", "abs2", "phasor", "interp_broadcast" ] def frame( x: np.ndarray, *, frame_length: int, hop_length: int, axis: int = -1, writeable: bool = False, subok: bool = False, ) -> np.ndarray: """Slice a data array into (overlapping) frames. This implementation uses low-level stride manipulation to avoid making a copy of the data. The resulting frame representation is a new view of the same input data. For example, a one-dimensional input ``x = [0, 1, 2, 3, 4, 5, 6]`` can be framed with frame length 3 and hop length 2 in two ways. The first (``axis=-1``), results in the array ``x_frames``:: [[0, 2, 4], [1, 3, 5], [2, 4, 6]] where each column ``x_frames[:, i]`` contains a contiguous slice of the input ``x[i * hop_length : i * hop_length + frame_length]``. The second way (``axis=0``) results in the array ``x_frames``:: [[0, 1, 2], [2, 3, 4], [4, 5, 6]] where each row ``x_frames[i]`` contains a contiguous slice of the input. This generalizes to higher dimensional inputs, as shown in the examples below. In general, the framing operation increments by 1 the number of dimensions, adding a new "frame axis" either before the framing axis (if ``axis < 0``) or after the framing axis (if ``axis >= 0``). Parameters ---------- x : np.ndarray Array to frame frame_length : int > 0 [scalar] Length of the frame hop_length : int > 0 [scalar] Number of steps to advance between frames axis : int The axis along which to frame. writeable : bool If ``False``, then the framed view of ``x`` is read-only. If ``True``, then the framed view is read-write. Note that writing to the framed view will also write to the input array ``x`` in this case. subok : bool If True, sub-classes will be passed-through, otherwise the returned array will be forced to be a base-class array (default). Returns ------- x_frames : np.ndarray [shape=(..., frame_length, N_FRAMES, ...)] A framed view of ``x``, for example with ``axis=-1`` (framing on the last dimension):: x_frames[..., j] == x[..., j * hop_length : j * hop_length + frame_length] If ``axis=0`` (framing on the first dimension), then:: x_frames[j] = x[j * hop_length : j * hop_length + frame_length] Raises ------ ParameterError If ``x.shape[axis] < frame_length``, there is not enough data to fill one frame. If ``hop_length < 1``, frames cannot advance. See Also -------- numpy.lib.stride_tricks.as_strided Examples -------- Extract 2048-sample frames from monophonic signal with a hop of 64 samples per frame >>> y, sr = librosa.loadx('trumpet') >>> frames = librosa.util.frame(y, frame_length=2048, hop_length=64) >>> frames array([[-1.407e-03, -2.604e-02, ..., -1.795e-05, -8.108e-06], [-4.461e-04, -3.721e-02, ..., -1.573e-05, -1.652e-05], ..., [ 7.960e-02, -2.335e-01, ..., -6.815e-06, 1.266e-05], [ 9.568e-02, -1.252e-01, ..., 7.397e-06, -1.921e-05]], dtype=float32) >>> y.shape (117601,) >>> frames.shape (2048, 1806) Or frame along the first axis instead of the last: >>> frames = librosa.util.frame(y, frame_length=2048, hop_length=64, axis=0) >>> frames.shape (1806, 2048) Frame a stereo signal: >>> y, sr = librosa.loadx('trumpet', mono=False) >>> y.shape (2, 117601) >>> frames = librosa.util.frame(y, frame_length=2048, hop_length=64) (2, 2048, 1806) Carve an STFT into fixed-length patches of 32 frames with 50% overlap >>> y, sr = librosa.loadx('trumpet') >>> S = np.abs(librosa.stft(y)) >>> S.shape (1025, 230) >>> S_patch = librosa.util.frame(S, frame_length=32, hop_length=16) >>> S_patch.shape (1025, 32, 13) >>> # The first patch contains the first 32 frames of S >>> np.allclose(S_patch[:, :, 0], S[:, :32]) True >>> # The second patch contains frames 16 to 16+32=48, and so on >>> np.allclose(S_patch[:, :, 1], S[:, 16:48]) True """ # This implementation is derived from numpy.lib.stride_tricks.sliding_window_view (1.20.0) # https://numpy.org/doc/stable/reference/generated/numpy.lib.stride_tricks.sliding_window_view.html x = np.array(x, copy=False, subok=subok) if x.shape[axis] < frame_length: raise ParameterError( f"Input is too short (n={x.shape[axis]:d}) for frame_length={frame_length:d}" ) if hop_length < 1: raise ParameterError(f"Invalid hop_length: {hop_length:d}") # put our new within-frame axis at the end for now out_strides = x.strides + tuple([x.strides[axis]]) # Reduce the shape on the framing axis x_shape_trimmed = list(x.shape) x_shape_trimmed[axis] -= frame_length - 1 out_shape = tuple(x_shape_trimmed) + tuple([frame_length]) xw = as_strided( x, strides=out_strides, shape=out_shape, subok=subok, writeable=writeable ) if axis < 0: target_axis = axis - 1 else: target_axis = axis + 1 xw = np.moveaxis(xw, -1, target_axis) # Downsample along the target axis slices = [slice(None)] * xw.ndim slices[axis] = slice(0, None, hop_length) return xw[tuple(slices)] @cache(level=20) def valid_audio(y: np.ndarray) -> bool: """Determine whether a variable contains valid audio data. The following conditions must be satisfied: - ``type(y)`` is ``np.ndarray`` - ``y.dtype`` is floating-point - ``y.ndim != 0`` (must have at least one dimension) - ``np.isfinite(y).all()`` samples must be all finite values Parameters ---------- y : np.ndarray The input data to validate Returns ------- valid : bool True if all tests pass Raises ------ ParameterError In any of the conditions specified above fails Notes ----- This function caches at level 20. Examples -------- We can make an array that can be interpreted as an audio signal >>> y = np.random.randn(5000) >>> librosa.util.valid_audio(y) True If we insert a non-finite sample value somewhere, it will fail >>> y[5] = np.nan >>> librosa.util.valid_audio(y) ... ParameterError: Audio buffer is not finite everywhere See Also -------- numpy.float32 """ if not isinstance(y, np.ndarray): raise ParameterError("Audio data must be of type numpy.ndarray") if not np.issubdtype(y.dtype, np.floating): raise ParameterError("Audio data must be floating-point") if y.ndim == 0: raise ParameterError( f"Audio data must be at least one-dimensional, given y.shape={y.shape}" ) if not np.isfinite(y).all(): raise ParameterError("Audio buffer is not finite everywhere") return True def valid_int(x: float, *, cast: Callable[[float], float] | None = None) -> int: """Ensure that an input value is integer-typed. This is primarily useful for ensuring integrable-valued array indices. Parameters ---------- x : number A scalar value to be cast to int cast : function [optional] A function to modify ``x`` before casting. Default: `np.floor` Returns ------- x_int : int ``x_int = int(cast(x))`` Raises ------ ParameterError If ``cast`` is provided and is not callable. """ if cast is None: cast = np.floor if not callable(cast): raise ParameterError("cast parameter must be callable") return int(cast(x)) def is_positive_int(x: float) -> bool: """Check that x is a positive integer, i.e. 1 or greater. Parameters ---------- x : number A scalar value to check Returns ------- positive : bool True if x is a positive integer, False otherwise """ # Check type first to catch None values. return isinstance(x, (int, np.integer)) and (x > 0) def valid_intervals(intervals: np.ndarray) -> bool: """Ensure that an array is a valid representation of time intervals: - intervals.ndim == 2 - intervals.shape[1] == 2 - intervals[i, 0] <= intervals[i, 1] for all i Parameters ---------- intervals : np.ndarray [shape=(n, 2)] set of time intervals Returns ------- valid : bool True if ``intervals`` passes validation. """ if intervals.ndim != 2 or intervals.shape[-1] != 2: raise ParameterError("intervals must have shape (n, 2)") if np.any(intervals[:, 0] > intervals[:, 1]): raise ParameterError(f"intervals={intervals} must have non-negative durations") return True def pad_center( data: np.ndarray, *, size: int, axis: int = -1, **kwargs: Any ) -> np.ndarray: """Pad an array to a target length along a target axis. This differs from `np.pad` by centering the data prior to padding, analogous to `str.center` Examples -------- >>> # Generate a vector >>> data = np.ones(5) >>> librosa.util.pad_center(data, size=10, mode='constant') array([ 0., 0., 1., 1., 1., 1., 1., 0., 0., 0.]) >>> # Pad a matrix along its first dimension >>> data = np.ones((3, 5)) >>> librosa.util.pad_center(data, size=7, axis=0) array([[ 0., 0., 0., 0., 0.], [ 0., 0., 0., 0., 0.], [ 1., 1., 1., 1., 1.], [ 1., 1., 1., 1., 1.], [ 1., 1., 1., 1., 1.], [ 0., 0., 0., 0., 0.], [ 0., 0., 0., 0., 0.]]) >>> # Or its second dimension >>> librosa.util.pad_center(data, size=7, axis=1) array([[ 0., 1., 1., 1., 1., 1., 0.], [ 0., 1., 1., 1., 1., 1., 0.], [ 0., 1., 1., 1., 1., 1., 0.]]) Parameters ---------- data : np.ndarray Vector to be padded and centered size : int >= len(data) [scalar] Length to pad ``data`` axis : int Axis along which to pad and center the data **kwargs : additional keyword arguments arguments passed to `np.pad` Returns ------- data_padded : np.ndarray ``data`` centered and padded to length ``size`` along the specified axis Raises ------ ParameterError If ``size < data.shape[axis]`` See Also -------- numpy.pad """ kwargs.setdefault("mode", "constant") n = data.shape[axis] lpad = int((size - n) // 2) lengths = [(0, 0)] * data.ndim lengths[axis] = (lpad, int(size - n - lpad)) if lpad < 0: raise ParameterError( f"Target size ({size:d}) must be at least input size ({n:d})" ) return np.pad(data, lengths, **kwargs) def expand_to( x: np.ndarray, *, ndim: int, axes: int | slice | Sequence[int] | Sequence[slice] ) -> np.ndarray: """Expand the dimensions of an input array with Parameters ---------- x : np.ndarray The input array ndim : int The number of dimensions to expand to. Must be at least ``x.ndim`` axes : int or slice The target axis or axes to preserve from x. All other axes will have length 1. Returns ------- x_exp : np.ndarray The expanded version of ``x``, satisfying the following: ``x_exp[axes] == x`` ``x_exp.ndim == ndim`` See Also -------- np.expand_dims Examples -------- Expand a 1d array into an (n, 1) shape >>> x = np.arange(3) >>> librosa.util.expand_to(x, ndim=2, axes=0) array([[0], [1], [2]]) Expand a 1d array into a (1, n) shape >>> librosa.util.expand_to(x, ndim=2, axes=1) array([[0, 1, 2]]) Expand a 2d array into (1, n, m, 1) shape >>> x = np.vander(np.arange(3)) >>> librosa.util.expand_to(x, ndim=4, axes=[1,2]).shape (1, 3, 3, 1) """ # Force axes into a tuple axes_tup: tuple[int] try: axes_tup = tuple(axes) # type: ignore except TypeError: axes_tup = tuple([axes]) # type: ignore if len(axes_tup) != x.ndim: raise ParameterError( f"Shape mismatch between axes={axes_tup} and input x.shape={x.shape}" ) if ndim < x.ndim: raise ParameterError( f"Cannot expand x.shape={x.shape} to fewer dimensions ndim={ndim}" ) shape: list[int] = [1] * ndim for i, axi in enumerate(axes_tup): shape[axi] = x.shape[i] return x.reshape(shape) def fix_length( data: np.ndarray, *, size: int, axis: int = -1, **kwargs: Any ) -> np.ndarray: """Fix the length an array ``data`` to exactly ``size`` along a target axis. If ``data.shape[axis] < n``, pad according to the provided kwargs. By default, ``data`` is padded with trailing zeros. Examples -------- >>> y = np.arange(7) >>> # Default: pad with zeros >>> librosa.util.fix_length(y, size=10) array([0, 1, 2, 3, 4, 5, 6, 0, 0, 0]) >>> # Trim to a desired length >>> librosa.util.fix_length(y, size=5) array([0, 1, 2, 3, 4]) >>> # Use edge-padding instead of zeros >>> librosa.util.fix_length(y, size=10, mode='edge') array([0, 1, 2, 3, 4, 5, 6, 6, 6, 6]) Parameters ---------- data : np.ndarray array to be length-adjusted size : int >= 0 [scalar] desired length of the array axis : int, <= data.ndim axis along which to fix length **kwargs : additional keyword arguments Parameters to ``np.pad`` Returns ------- data_fixed : np.ndarray [shape=data.shape] ``data`` either trimmed or padded to length ``size`` along the specified axis. See Also -------- numpy.pad """ kwargs.setdefault("mode", "constant") n = data.shape[axis] if n > size: slices = [slice(None)] * data.ndim slices[axis] = slice(0, size) return data[tuple(slices)] elif n < size: lengths = [(0, 0)] * data.ndim lengths[axis] = (0, size - n) return np.pad(data, lengths, **kwargs) return data def fix_frames( frames: _SequenceLike[int], *, x_min: int | None = 0, x_max: int | None = None, pad: bool = True, ) -> _Array1D[np.int_]: """Fix a list of frames to lie within [x_min, x_max] Examples -------- >>> # Generate a list of frame indices >>> frames = np.arange(0, 1000.0, 50) >>> frames array([ 0., 50., 100., 150., 200., 250., 300., 350., 400., 450., 500., 550., 600., 650., 700., 750., 800., 850., 900., 950.]) >>> # Clip to span at most 250 >>> librosa.util.fix_frames(frames, x_max=250) array([ 0, 50, 100, 150, 200, 250]) >>> # Or pad to span up to 2500 >>> librosa.util.fix_frames(frames, x_max=2500) array([ 0, 50, 100, 150, 200, 250, 300, 350, 400, 450, 500, 550, 600, 650, 700, 750, 800, 850, 900, 950, 2500]) >>> librosa.util.fix_frames(frames, x_max=2500, pad=False) array([ 0, 50, 100, 150, 200, 250, 300, 350, 400, 450, 500, 550, 600, 650, 700, 750, 800, 850, 900, 950]) >>> # Or starting away from zero >>> frames = np.arange(200, 500, 33) >>> frames array([200, 233, 266, 299, 332, 365, 398, 431, 464, 497]) >>> librosa.util.fix_frames(frames) array([ 0, 200, 233, 266, 299, 332, 365, 398, 431, 464, 497]) >>> librosa.util.fix_frames(frames, x_max=500) array([ 0, 200, 233, 266, 299, 332, 365, 398, 431, 464, 497, 500]) Parameters ---------- frames : np.ndarray [shape=(n_frames,)] List of non-negative frame indices x_min : int >= 0 or None Minimum allowed frame index x_max : int >= 0 or None Maximum allowed frame index pad : bool If ``True``, then ``frames`` is expanded to span the full range ``[x_min, x_max]`` Returns ------- fixed_frames : np.ndarray [shape=(n_fixed_frames,), dtype=int] Fixed frame indices, flattened and sorted Raises ------ ParameterError If ``frames`` contains negative values """ frames = np.asarray(frames) if np.any(frames < 0): raise ParameterError("Negative frame index detected") # TODO: this whole function could be made more efficient if pad and (x_min is not None or x_max is not None): frames = np.clip(frames, x_min, x_max) if pad: pad_data = [] if x_min is not None: pad_data.append(x_min) if x_max is not None: pad_data.append(x_max) frames = np.concatenate((np.asarray(pad_data), frames)) if x_min is not None: frames = frames[frames >= x_min] if x_max is not None: frames = frames[frames <= x_max] unique: np.ndarray = np.unique(frames).astype(int) return unique @overload def axis_sort( S: np.ndarray, *, axis: int = ..., index: Literal[False] = ..., value: Callable[..., Any] | None = ..., ) -> _Array2D[Any]: ... @overload def axis_sort( S: np.ndarray, *, axis: int = ..., index: Literal[True], value: Callable[..., Any] | None = ..., ) -> tuple[_Array2D[Any], _Array1D[np.int_]]: ... def axis_sort( S: np.ndarray, *, axis: int = -1, index: bool = False, value: Callable[..., Any] | None = None, ) -> _Array2D[Any] | tuple[_Array2D[Any], _Array1D[np.int_]]: """Sort an array along its rows or columns. Examples -------- Visualize NMF output for a spectrogram S >>> # Sort the columns of W by peak frequency bin >>> y, sr = librosa.loadx('trumpet') >>> S = np.abs(librosa.stft(y)) >>> W, H = librosa.decompose.decompose(S, n_components=64) >>> W_sort = librosa.util.axis_sort(W) Or sort by the lowest frequency bin >>> W_sort = librosa.util.axis_sort(W, value=np.argmin) Or sort the rows instead of the columns >>> W_sort_rows = librosa.util.axis_sort(W, axis=0) Get the sorting index also, and use it to permute the rows of H >>> W_sort, idx = librosa.util.axis_sort(W, index=True) >>> H_sort = H[idx, :] >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(nrows=2, ncols=2) >>> img_w = librosa.display.specshow(W, vscale='dBFS', ... y_axis='log', ax=ax[0, 0]) >>> ax[0, 0].set(title='W') >>> ax[0, 0].label_outer() >>> img_act = librosa.display.specshow(H, x_axis='time', ax=ax[0, 1]) >>> ax[0, 1].set(title='H') >>> ax[0, 1].label_outer() >>> librosa.display.specshow(W_sort, vscale='dBFS', ... y_axis='log', ax=ax[1, 0]) >>> ax[1, 0].set(title='W sorted') >>> librosa.display.specshow(H_sort, x_axis='time', ax=ax[1, 1]) >>> ax[1, 1].set(title='H sorted') >>> ax[1, 1].label_outer() >>> cbar = librosa.display.colorbar_db(img_w, ax=ax[:, 0], orientation='horizontal') >>> cbar.ax.tick_params("x", rotation=45) >>> fig.colorbar(img_act, ax=ax[:, 1], orientation='horizontal') Parameters ---------- S : np.ndarray [shape=(d, n)] Array to be sorted axis : int [scalar] The axis along which to compute the sorting values - ``axis=0`` to sort rows by peak column index - ``axis=1`` to sort columns by peak row index index : bool [scalar] If true, returns the index array as well as the permuted data. value : function function to return the index corresponding to the sort order. Default: `np.argmax`. Returns ------- S_sort : np.ndarray [shape=(d, n)] ``S`` with the columns or rows permuted in sorting order idx : np.ndarray (optional) [shape=(d,) or (n,)] If ``index == True``, the sorting index used to permute ``S``. Length of ``idx`` corresponds to the selected ``axis``. Raises ------ ParameterError If ``S`` does not have exactly 2 dimensions (``S.ndim != 2``) """ if value is None: value = np.argmax if S.ndim != 2: raise ParameterError("axis_sort is only defined for 2D arrays") bin_idx = value(S, axis=np.mod(1 - axis, S.ndim)) idx = np.argsort(bin_idx) sort_slice = [slice(None)] * S.ndim sort_slice[axis] = idx # type: ignore if index: return S[tuple(sort_slice)], idx else: return S[tuple(sort_slice)] @cache(level=40) def normalize( S: np.ndarray, *, norm: float | None = np.inf, axis: int | None = 0, threshold: _FloatLike_co | None = None, fill: bool | None = None, ) -> np.ndarray: """Normalize an array along a chosen axis. Given a norm (described below) and a target axis, the input array is scaled so that:: norm(S, axis=axis) == 1 For example, ``axis=0`` normalizes each column of a 2-d array by aggregating over the rows (0-axis). Similarly, ``axis=1`` normalizes each row of a 2-d array. This function also supports thresholding small-norm slices: any slice (i.e., row or column) with norm below a specified ``threshold`` can be left un-normalized, set to all-zeros, or filled with uniform non-zero values that normalize to 1. Note: the semantics of this function differ from `scipy.linalg.norm` in two ways: multi-dimensional arrays are supported, but matrix-norms are not. Parameters ---------- S : np.ndarray The array to normalize norm : {np.inf, -np.inf, 0, float > 0, None} - `np.inf` : maximum absolute value - `-np.inf` : minimum absolute value - `0` : number of non-zeros (the support) - float : corresponding l_p norm See `scipy.linalg.norm` for details. - None : no normalization is performed axis : int [scalar] Axis along which to compute the norm. threshold : number > 0 [optional] Only the columns (or rows) with norm at least ``threshold`` are normalized. By default, the threshold is determined from the numerical precision of ``S.dtype``. fill : None or bool If None, then columns (or rows) with norm below ``threshold`` are left as is. If False, then columns (rows) with norm below ``threshold`` are set to 0. If True, then columns (rows) with norm below ``threshold`` are filled uniformly such that the corresponding norm is 1. .. note:: ``fill=True`` is incompatible with ``norm=0`` because no uniform vector exists with l0 "norm" equal to 1. Returns ------- S_norm : np.ndarray [shape=S.shape] Normalized array Raises ------ ParameterError If ``norm`` is not among the valid types defined above If ``S`` is not finite If ``fill=True`` and ``norm=0`` See Also -------- scipy.linalg.norm Notes ----- This function caches at level 40. Examples -------- >>> # Construct an example matrix >>> S = np.vander(np.arange(-2.0, 2.0)) >>> S array([[-8., 4., -2., 1.], [-1., 1., -1., 1.], [ 0., 0., 0., 1.], [ 1., 1., 1., 1.]]) >>> # Max (l-infinity)-normalize the columns >>> librosa.util.normalize(S) array([[-1. , 1. , -1. , 1. ], [-0.125, 0.25 , -0.5 , 1. ], [ 0. , 0. , 0. , 1. ], [ 0.125, 0.25 , 0.5 , 1. ]]) >>> # Max (l-infinity)-normalize the rows >>> librosa.util.normalize(S, axis=1) array([[-1. , 0.5 , -0.25 , 0.125], [-1. , 1. , -1. , 1. ], [ 0. , 0. , 0. , 1. ], [ 1. , 1. , 1. , 1. ]]) >>> # l1-normalize the columns >>> librosa.util.normalize(S, norm=1) array([[-0.8 , 0.667, -0.5 , 0.25 ], [-0.1 , 0.167, -0.25 , 0.25 ], [ 0. , 0. , 0. , 0.25 ], [ 0.1 , 0.167, 0.25 , 0.25 ]]) >>> # l2-normalize the columns >>> librosa.util.normalize(S, norm=2) array([[-0.985, 0.943, -0.816, 0.5 ], [-0.123, 0.236, -0.408, 0.5 ], [ 0. , 0. , 0. , 0.5 ], [ 0.123, 0.236, 0.408, 0.5 ]]) >>> # Thresholding and filling >>> S[:, -1] = 1e-308 >>> S array([[ -8.000e+000, 4.000e+000, -2.000e+000, 1.000e-308], [ -1.000e+000, 1.000e+000, -1.000e+000, 1.000e-308], [ 0.000e+000, 0.000e+000, 0.000e+000, 1.000e-308], [ 1.000e+000, 1.000e+000, 1.000e+000, 1.000e-308]]) >>> # By default, small-norm columns are left untouched >>> librosa.util.normalize(S) array([[ -1.000e+000, 1.000e+000, -1.000e+000, 1.000e-308], [ -1.250e-001, 2.500e-001, -5.000e-001, 1.000e-308], [ 0.000e+000, 0.000e+000, 0.000e+000, 1.000e-308], [ 1.250e-001, 2.500e-001, 5.000e-001, 1.000e-308]]) >>> # Small-norm columns can be zeroed out >>> librosa.util.normalize(S, fill=False) array([[-1. , 1. , -1. , 0. ], [-0.125, 0.25 , -0.5 , 0. ], [ 0. , 0. , 0. , 0. ], [ 0.125, 0.25 , 0.5 , 0. ]]) >>> # Or set to constant with unit-norm >>> librosa.util.normalize(S, fill=True) array([[-1. , 1. , -1. , 1. ], [-0.125, 0.25 , -0.5 , 1. ], [ 0. , 0. , 0. , 1. ], [ 0.125, 0.25 , 0.5 , 1. ]]) >>> # With an l1 norm instead of max-norm >>> librosa.util.normalize(S, norm=1, fill=True) array([[-0.8 , 0.667, -0.5 , 0.25 ], [-0.1 , 0.167, -0.25 , 0.25 ], [ 0. , 0. , 0. , 0.25 ], [ 0.1 , 0.167, 0.25 , 0.25 ]]) """ # Avoid div-by-zero if threshold is None: threshold = tiny(S) elif threshold <= 0: raise ParameterError(f"threshold={threshold} must be strictly positive") if fill not in [None, False, True]: raise ParameterError(f"fill={fill} must be None or boolean") if not np.all(np.isfinite(S)): raise ParameterError("Input must be finite") # All norms only depend on magnitude, let's do that first mag = np.abs(S).astype(float) # For max/min norms, filling with 1 works fill_norm = 1 if norm is None: return S elif norm == np.inf: length = np.max(mag, axis=axis, keepdims=True) elif norm == -np.inf: length = np.min(mag, axis=axis, keepdims=True) elif norm == 0: if fill is True: raise ParameterError("Cannot normalize with norm=0 and fill=True") length = np.sum(mag > 0, axis=axis, keepdims=True, dtype=mag.dtype) elif np.issubdtype(type(norm), np.number) and norm > 0: length = np.sum(mag**norm, axis=axis, keepdims=True) ** (1.0 / norm) if axis is None: fill_norm = mag.size ** (-1.0 / norm) else: fill_norm = mag.shape[axis] ** (-1.0 / norm) else: raise ParameterError(f"Unsupported norm: {norm!r}") # indices where norm is below the threshold small_idx = length < threshold Snorm = np.empty_like(S) if fill is None: # Leave small indices un-normalized length[small_idx] = 1.0 Snorm[:] = S / length elif fill: # If we have a non-zero fill value, we locate those entries by # doing a nan-divide. # If S was finite, then length is finite (except for small positions) length[small_idx] = np.nan Snorm[:] = S / length Snorm[np.isnan(Snorm)] = fill_norm else: # Set small values to zero by doing an inf-divide. # This is safe (by IEEE-754) as long as S is finite. length[small_idx] = np.inf Snorm[:] = S / length return Snorm @numba.stencil def _localmax_sten(x): # pragma: no cover """Numba stencil for local maxima computation""" return (x[0] > x[-1]) & (x[0] >= x[1]) @numba.stencil def _localmin_sten(x): # pragma: no cover """Numba stencil for local minima computation""" return (x[0] < x[-1]) & (x[0] <= x[1]) @numba.guvectorize( "(n)->(n)", cache=True, nopython=True, ) def _localmax(x, y): # pragma: no cover """Vectorized wrapper for the localmax stencil""" y[:] = _localmax_sten(x) @numba.guvectorize( "(n)->(n)", cache=True, nopython=True, ) def _localmin(x, y): # pragma: no cover """Vectorized wrapper for the localmin stencil""" y[:] = _localmin_sten(x) def localmax(x: np.ndarray, *, axis: int = 0) -> NDArray[np.bool]: """Find local maxima in an array An element ``x[i]`` is considered a local maximum if the following conditions are met: - ``x[i] > x[i-1]`` - ``x[i] >= x[i+1]`` Note that the first condition is strict, and that the first element ``x[0]`` will never be considered as a local maximum. Examples -------- >>> x = np.array([1, 0, 1, 2, -1, 0, -2, 1]) >>> librosa.util.localmax(x) array([False, False, False, True, False, True, False, True], dtype=bool) >>> # Two-dimensional example >>> x = np.array([[1,0,1], [2, -1, 0], [2, 1, 3]]) >>> librosa.util.localmax(x, axis=0) array([[False, False, False], [ True, False, False], [False, True, True]], dtype=bool) >>> librosa.util.localmax(x, axis=1) array([[False, False, True], [False, False, True], [False, False, True]], dtype=bool) Parameters ---------- x : np.ndarray [shape=(d1,d2,...)] input vector or array axis : int axis along which to compute local maximality Returns ------- m : np.ndarray [shape=x.shape, dtype=bool] indicator array of local maximality along ``axis`` See Also -------- localmin """ # Rotate the target axis to the end xi = x.swapaxes(-1, axis) # Allocate the output array and rotate target axis lmax = np.empty_like(x, dtype=bool) lmaxi = lmax.swapaxes(-1, axis) # Call the vectorized stencil _localmax(xi, lmaxi) # Handle the edge condition not covered by the stencil lmaxi[..., -1] = xi[..., -1] > xi[..., -2] return lmax def localmin(x: np.ndarray, *, axis: int = 0) -> NDArray[np.bool]: """Find local minima in an array An element ``x[i]`` is considered a local minimum if the following conditions are met: - ``x[i] < x[i-1]`` - ``x[i] <= x[i+1]`` Note that the first condition is strict, and that the first element ``x[0]`` will never be considered as a local minimum. Examples -------- >>> x = np.array([1, 0, 1, 2, -1, 0, -2, 1]) >>> librosa.util.localmin(x) array([False, True, False, False, True, False, True, False]) >>> # Two-dimensional example >>> x = np.array([[1,0,1], [2, -1, 0], [2, 1, 3]]) >>> librosa.util.localmin(x, axis=0) array([[False, False, False], [False, True, True], [False, False, False]]) >>> librosa.util.localmin(x, axis=1) array([[False, True, False], [False, True, False], [False, True, False]]) Parameters ---------- x : np.ndarray [shape=(d1,d2,...)] input vector or array axis : int axis along which to compute local minimality Returns ------- m : np.ndarray [shape=x.shape, dtype=bool] indicator array of local minimality along ``axis`` See Also -------- localmax """ # Rotate the target axis to the end xi = x.swapaxes(-1, axis) # Allocate the output array and rotate target axis lmin = np.empty_like(x, dtype=bool) lmini = lmin.swapaxes(-1, axis) # Call the vectorized stencil _localmin(xi, lmini) # Handle the edge condition not covered by the stencil lmini[..., -1] = xi[..., -1] < xi[..., -2] return lmin @numba.guvectorize( "(n),(),(),(),(),(),()->(n)", nopython=True, cache=True, ) def __peak_pick_greedy(x, pre_max, post_max, pre_avg, post_avg, delta, wait, peaks): """Vectorized wrapper for the greedy peak-picker""" # Special case the first frame peaks[0] = x[0] >= np.max(x[: min(post_max, x.shape[0])]) peaks[0] &= x[0] >= np.mean(x[: min(post_avg, x.shape[0])]) + delta if peaks[0]: n = wait + 1 else: n = 1 while n < x.shape[0]: maxn = np.max(x[max(0, n - pre_max) : min(n + post_max, x.shape[0])]) # Are we the local max and sufficiently above average? peaks[n] = x[n] == maxn if not peaks[n]: n += 1 continue avgn = np.mean(x[max(0, n - pre_avg) : min(n + post_avg, x.shape[0])]) peaks[n] &= x[n] >= avgn + delta if not peaks[n]: n += 1 continue # Skip the next `wait` frames n += wait + 1 @numba.guvectorize( "(n),(),(),(),(),(),(),()->(n)", nopython=True, cache=True, ) def __peak_pick_dp(x, pre_max, post_max, pre_avg, post_avg, delta, wait, count, peaks): """Vectorized wrapper for optimal peak-picker by dynamic programming All parameters are the same as for `peak_pick`, except for `count` and `peaks`. `count` is a boolean that indicates whether to maximize the number of peaks or the sum of their values. `peaks` is the pre-allocated output array. """ values = np.zeros(len(x) + 1) pointers = np.zeros(len(x) + 1, dtype=np.int32) taken = np.zeros(len(x) + 1, dtype=np.bool_) # Use the integral image trick to accelerate partial sums for averages cumulate = np.cumsum(x) values[-1] = 0 pointers[-1] = -1 for n in range(len(x) - 1, -1, -1): # Populate defaults in case we don't take this peak values[n] = values[n + 1] pointers[n] = n + 1 # Check if we're a local peak maxn = np.max(x[max(0, n - pre_max) : min(n + post_max, x.shape[0])]) # if not a peak, move along if x[n] < maxn: continue # Are we enough above average? idx_prev = max(0, n - pre_avg) idx_post = min(n + post_avg, x.shape[0]) if idx_prev == 0: avgn = cumulate[idx_post - 1] / idx_post else: avgn = (cumulate[idx_post - 1] - cumulate[idx_prev - 1]) / (idx_post - idx_prev) if count: v = 1 else: v = x[n] next_ptr = min(len(x), n + wait + 1) # Only take this peak if it's better than not taking it if x[n] >= avgn + delta and values[next_ptr] + v > values[n + 1]: values[n] = values[next_ptr] + v pointers[n] = next_ptr taken[n] = True # Backtrack to find the selected peaks n = 0 while pointers[n] >= 0: peaks[n] = taken[n] n = pointers[n] @overload def peak_pick( x: np.ndarray, *, pre_max: int, post_max: int, pre_avg: int, post_avg: int, delta: float, wait: int, sparse: Literal[False], method: _PeakPickMethod = "greedy", axis: int = -1, ) -> NDArray[np.bool]: ... @overload def peak_pick( x: np.ndarray, *, pre_max: int, post_max: int, pre_avg: int, post_avg: int, delta: float, wait: int, sparse: Literal[True] = True, method: _PeakPickMethod = "greedy", axis: int = -1, ) -> NDArray[np.int_]: ... @overload # this overload should not be needed, but mypy becomes confused without it def peak_pick( x: np.ndarray, *, pre_max: int, post_max: int, pre_avg: int, post_avg: int, delta: float, wait: int, sparse: bool = True, method: _PeakPickMethod = "greedy", axis: int = -1, ) -> NDArray[np.bool | np.int_]: ... def peak_pick( x: np.ndarray, *, pre_max: int, post_max: int, pre_avg: int, post_avg: int, delta: float, wait: int, sparse: bool = True, method: _PeakPickMethod = "greedy", axis: int = -1, ) -> NDArray[np.bool | np.int_]: """Use a flexible heuristic to pick peaks in a signal. A sample n is selected as an peak if the corresponding ``x[n]`` fulfills the following three conditions: 1. ``x[n] == max(x[n - pre_max:n + post_max])`` 2. ``x[n] >= mean(x[n - pre_avg:n + post_avg]) + delta`` 3. ``n - previous_n > wait`` where ``previous_n`` is the last sample picked as a peak (greedily). This implementation is based on [#]_ and [#]_. .. [#] Boeck, Sebastian, Florian Krebs, and Markus Schedl. "Evaluating the Online Capabilities of Onset Detection Methods." ISMIR. 2012. .. [#] https://github.com/CPJKU/onset_detection/blob/master/onset_program.py Parameters ---------- x : np.ndarray input signal to peak picks from pre_max : int >= 0 [scalar] number of samples before ``n`` over which max is computed post_max : int >= 1 [scalar] number of samples after ``n`` over which max is computed pre_avg : int >= 0 [scalar] number of samples before ``n`` over which mean is computed post_avg : int >= 1 [scalar] number of samples after ``n`` over which mean is computed delta : float >= 0 [scalar] threshold offset for mean wait : int >= 0 [scalar] number of samples to wait after picking a peak sparse : bool [scalar] If `True`, the output are indices of detected peaks. If `False`, the output is a dense boolean array of the same shape as ``x``. method : {'greedy', 'dp_count', 'dp_value'} [scalar] The method used to pick peaks. The default is 'greedy', which implements the method of Böck et al. (2012). The greedy method selects the earliest possible peaks (i.e. those with minimal index values) subject to the constraints described above. The 'dp_*' methods implement a dynamic programming method which seeks to explicitly maximize either the number of selected peaks (`dp_count`) or the sum of the values `x[p]` the selected peaks `p` (`dp_value`). axis : int [scalar] the axis over which to detect peaks. Returns ------- peaks : np.ndarray [shape=(n_peaks,) or shape=x.shape, dtype=int or bool] indices of peaks in ``x`` (sparse=True) or a boolean array where `peaks[..., n]` indicates a peak at frame index `n` (sparse=False) Raises ------ ParameterError If any input lies outside its defined range Examples -------- >>> y, sr = librosa.loadx('trumpet') >>> onset_env = librosa.onset.onset_strength(y=y, sr=sr, ... hop_length=512, ... aggregate=np.median) >>> peaks = librosa.util.peak_pick(onset_env, pre_max=3, post_max=3, pre_avg=3, post_avg=5, delta=0.5, wait=10) >>> peaks array([ 3, 27, 40, 61, 72, 88, 103]) Using dense output to make a boolean array of peak indicators >>> librosa.util.peak_pick(onset_env, pre_max=3, post_max=3, pre_avg=3, post_avg=5, ... delta=0.5, wait=10, sparse=False) array([False, False, ..., False, False]) >>> import matplotlib.pyplot as plt >>> times = librosa.times_like(onset_env, sr=sr, hop_length=512) >>> fig, ax = plt.subplots(nrows=2, sharex=True) >>> D = np.abs(librosa.stft(y)) >>> librosa.display.specshow(D, vscale='dBFS', ... y_axis='log', x_axis='time', ax=ax[1]) >>> ax[0].plot(times, onset_env, alpha=0.8, label='Onset strength') >>> ax[0].vlines(times[peaks], 0, ... onset_env.max(), color='r', alpha=0.8, ... label='Selected peaks') >>> ax[0].legend(frameon=True, framealpha=0.8) >>> ax[0].label_outer() """ if pre_max < 0: raise ParameterError("pre_max must be non-negative") if pre_avg < 0: raise ParameterError("pre_avg must be non-negative") if delta < 0: raise ParameterError("delta must be non-negative") if wait < 0: raise ParameterError("wait must be non-negative") if post_max <= 0: raise ParameterError("post_max must be positive") if post_avg <= 0: raise ParameterError("post_avg must be positive") if sparse and x.ndim != 1: raise ParameterError( f"sparse=True (default) does not support " f"{x.ndim}-dimensional inputs. " f"Either set sparse=False or process each dimension independently." ) # Ensure valid index types pre_max = valid_int(pre_max, cast=np.ceil) post_max = valid_int(post_max, cast=np.ceil) pre_avg = valid_int(pre_avg, cast=np.ceil) post_avg = valid_int(post_avg, cast=np.ceil) wait = valid_int(wait, cast=np.ceil) peaks = np.zeros_like(x, dtype=bool) if method == "greedy": __peak_pick_greedy( x.swapaxes(axis, -1), pre_max, post_max, pre_avg, post_avg, delta, wait, peaks.swapaxes(axis, -1), ) elif method == "dp_count": __peak_pick_dp( x.swapaxes(axis, -1), pre_max, post_max, pre_avg, post_avg, delta, wait, True, peaks.swapaxes(axis, -1), ) elif method == "dp_value": __peak_pick_dp( x.swapaxes(axis, -1), pre_max, post_max, pre_avg, post_avg, delta, wait, False, peaks.swapaxes(axis, -1), ) else: raise ParameterError(f"Unknown method {method}") if sparse: return np.flatnonzero(peaks) return peaks @cache(level=40) def sparsify_rows( x: np.ndarray, *, quantile: float = 0.01, dtype: DTypeLike | None = None ) -> scipy.sparse.csr_array: """Return a row-sparse array approximating the input Parameters ---------- x : np.ndarray [ndim <= 2] The input array to sparsify. quantile : float in [0, 1.0) Percentage of magnitude to discard in each row of ``x`` dtype : np.dtype, optional The dtype of the output array. If not provided, then ``x.dtype`` will be used. Returns ------- x_sparse : ``scipy.sparse.csr_array`` [shape=x.shape] Row-sparsified approximation of ``x`` If ``x.ndim == 1``, then ``x`` is interpreted as a row vector, and ``x_sparse.shape == (1, len(x))``. Raises ------ ParameterError If ``x.ndim > 2`` If ``quantile`` lies outside ``[0, 1.0)`` Notes ----- This function caches at level 40. Examples -------- >>> # Construct a Hann window to sparsify >>> x = scipy.signal.hann(32) >>> x array([ 0. , 0.01 , 0.041, 0.09 , 0.156, 0.236, 0.326, 0.424, 0.525, 0.625, 0.72 , 0.806, 0.879, 0.937, 0.977, 0.997, 0.997, 0.977, 0.937, 0.879, 0.806, 0.72 , 0.625, 0.525, 0.424, 0.326, 0.236, 0.156, 0.09 , 0.041, 0.01 , 0. ]) >>> # Discard the bottom percentile >>> x_sparse = librosa.util.sparsify_rows(x, quantile=0.01) >>> x_sparse <1x32 sparse array of type '' with 26 stored elements in Compressed Sparse Row format> >>> x_sparse.toarray() array([[ 0. , 0. , 0. , 0.09 , 0.156, 0.236, 0.326, 0.424, 0.525, 0.625, 0.72 , 0.806, 0.879, 0.937, 0.977, 0.997, 0.997, 0.977, 0.937, 0.879, 0.806, 0.72 , 0.625, 0.525, 0.424, 0.326, 0.236, 0.156, 0.09 , 0. , 0. , 0. ]]) >>> # Discard up to the bottom 10th percentile >>> x_sparse = librosa.util.sparsify_rows(x, quantile=0.1) >>> x_sparse <1x32 sparse array of type '' with 20 stored elements in Compressed Sparse Row format> >>> x_sparse.toarray() array([[ 0. , 0. , 0. , 0. , 0. , 0. , 0.326, 0.424, 0.525, 0.625, 0.72 , 0.806, 0.879, 0.937, 0.977, 0.997, 0.997, 0.977, 0.937, 0.879, 0.806, 0.72 , 0.625, 0.525, 0.424, 0.326, 0. , 0. , 0. , 0. , 0. , 0. ]]) """ if x.ndim == 1: x = x.reshape((1, -1)) elif x.ndim > 2: raise ParameterError( f"Input must have 2 or fewer dimensions. Provided x.shape={x.shape}." ) if not 0.0 <= quantile < 1: raise ParameterError(f"Invalid quantile {quantile:.2f}") if dtype is None: dtype = x.dtype out_dtype = np.dtype(dtype) mags = np.abs(x) norms = np.sum(mags, axis=1, keepdims=True) mag_sort = np.sort(mags, axis=1) cumulative_mag = np.cumsum(mag_sort / norms, axis=1) threshold_idx = np.argmin(cumulative_mag < quantile, axis=1) # threshold value per row thresh = mag_sort[np.arange(x.shape[0]), threshold_idx] # (n_rows,) # mask-multiply mask = mags >= thresh[:, np.newaxis] out = (x * mask).astype(out_dtype, copy=False) return scipy.sparse.csr_array(out) def buf_to_float( x: np.ndarray, *, n_bytes: int = 2, dtype: DTypeLike = np.float32 ) -> np.ndarray: """Convert an integer buffer to floating point values. This is primarily useful when loading integer-valued wav data into numpy arrays. Parameters ---------- x : np.ndarray [dtype=int] The integer-valued data buffer n_bytes : int [1, 2, 4] The number of bytes per sample in ``x`` dtype : numeric type The target output type (default: 32-bit float) Returns ------- x_float : np.ndarray [dtype=float] The input data buffer cast to floating point """ # Invert the scale of the data scale = 1.0 / float(1 << ((8 * n_bytes) - 1)) # Construct the format string fmt = f" list[slice]: """Generate a slice array from an index array. Parameters ---------- idx : list-like Array of index boundaries idx_min, idx_max : None or int Minimum and maximum allowed indices step : None or int Step size for each slice. If `None`, then the default step of 1 is used. pad : bool If `True`, pad ``idx`` to span the range ``idx_min:idx_max``. Returns ------- slices : list of slice ``slices[i] = slice(idx[i], idx[i+1], step)`` Additional slice objects may be added at the beginning or end, depending on whether ``pad==True`` and the supplied values for ``idx_min`` and ``idx_max``. See Also -------- fix_frames Examples -------- >>> # Generate slices from spaced indices >>> librosa.util.index_to_slice(np.arange(20, 100, 15)) [slice(20, 35, None), slice(35, 50, None), slice(50, 65, None), slice(65, 80, None), slice(80, 95, None)] >>> # Pad to span the range (0, 100) >>> librosa.util.index_to_slice(np.arange(20, 100, 15), ... idx_min=0, idx_max=100) [slice(0, 20, None), slice(20, 35, None), slice(35, 50, None), slice(50, 65, None), slice(65, 80, None), slice(80, 95, None), slice(95, 100, None)] >>> # Use a step of 5 for each slice >>> librosa.util.index_to_slice(np.arange(20, 100, 15), ... idx_min=0, idx_max=100, step=5) [slice(0, 20, 5), slice(20, 35, 5), slice(35, 50, 5), slice(50, 65, 5), slice(65, 80, 5), slice(80, 95, 5), slice(95, 100, 5)] """ # First, normalize the index set idx_fixed = fix_frames(idx, x_min=idx_min, x_max=idx_max, pad=pad) # Now convert the indices to slices return [slice(start, end, step) for (start, end) in itertools.pairwise(idx_fixed)] @cache(level=40) def sync( data: np.ndarray, idx: Sequence[int] | Sequence[slice], *, aggregate: Callable[..., Any] | None = None, pad: bool = True, axis: int = -1, ) -> np.ndarray: """Aggregate a multi-dimensional array between specified boundaries. .. note:: In order to ensure total coverage, boundary points may be added to ``idx``. If synchronizing a feature matrix against beat tracker output, ensure that frame index numbers are properly aligned and use the same hop length. Parameters ---------- data : np.ndarray multi-dimensional array of features idx : sequence of ints or slices Either an ordered array of boundary indices, or an iterable collection of slice objects. aggregate : function aggregation function (default: `np.mean`) pad : bool If `True`, ``idx`` is padded to span the full range ``[0, data.shape[axis]]`` axis : int The axis along which to aggregate data Returns ------- data_sync : ndarray ``data_sync`` will have the same dimension as ``data``, except that the ``axis`` coordinate will be reduced according to ``idx``. For example, a 2-dimensional ``data`` with ``axis=-1`` should satisfy:: data_sync[:, i] = aggregate(data[:, idx[i-1]:idx[i]], axis=-1) Raises ------ ParameterError If the index set is not of consistent type (all slices or all integers) Notes ----- This function caches at level 40. Examples -------- Beat-synchronous CQT spectra >>> y, sr = librosa.loadx('choice') >>> tempo, beats = librosa.beat.beat_track(y=y, sr=sr, trim=False) >>> C = np.abs(librosa.cqt(y=y, sr=sr)) >>> beats = librosa.util.fix_frames(beats) By default, use mean aggregation >>> C_avg = librosa.util.sync(C, beats) Use median-aggregation instead of mean >>> C_med = librosa.util.sync(C, beats, ... aggregate=np.median) Or sub-beat synchronization >>> sub_beats = librosa.segment.subsegment(C, beats) >>> sub_beats = librosa.util.fix_frames(sub_beats) >>> C_med_sub = librosa.util.sync(C, sub_beats, aggregate=np.median) Plot the results >>> import matplotlib.pyplot as plt >>> beat_t = librosa.frames_to_time(beats, sr=sr) >>> subbeat_t = librosa.frames_to_time(sub_beats, sr=sr) >>> fig, ax = plt.subplots(nrows=3, sharex=True, sharey=True) >>> librosa.display.specshow(C, vscale='dBFS', ... x_axis='time', ax=ax[0]) >>> ax[0].set(title='CQT power, shape={}'.format(C.shape)) >>> ax[0].label_outer() >>> librosa.display.specshow(C_med, vscale='dBFS', ... x_coords=beat_t, x_axis='time', ax=ax[1]) >>> ax[1].set(title='Beat synchronous CQT power, ' ... 'shape={}'.format(C_med.shape)) >>> ax[1].label_outer() >>> librosa.display.specshow(C_med_sub, vscale='dBFS', ... x_coords=subbeat_t, x_axis='time', ax=ax[2]) >>> ax[2].set(title='Sub-beat synchronous CQT power, ' ... 'shape={}'.format(C_med_sub.shape)) """ if aggregate is None: aggregate = np.mean shape = list(data.shape) if np.all([isinstance(_, slice) for _ in idx]): slices = idx elif np.all([np.issubdtype(type(_), np.integer) for _ in idx]): slices = index_to_slice( np.asarray(idx), idx_min=0, idx_max=shape[axis], pad=pad ) else: raise ParameterError(f"Invalid index set: {idx}") agg_shape = list(shape) agg_shape[axis] = len(slices) data_agg = np.empty( agg_shape, order="F" if np.isfortran(data) else "C", dtype=data.dtype ) idx_in = [slice(None)] * data.ndim idx_agg = [slice(None)] * data_agg.ndim for i, segment in enumerate(slices): idx_in[axis] = segment # type: ignore idx_agg[axis] = i # type: ignore data_agg[tuple(idx_agg)] = aggregate(data[tuple(idx_in)], axis=axis) return data_agg def softmask( X: np.ndarray, X_ref: np.ndarray, *, power: float = 1, split_zeros: bool = False ) -> np.ndarray: """Robustly compute a soft-mask operation. ``M = X**power / (X**power + X_ref**power)`` Parameters ---------- X : np.ndarray The (non-negative) input array corresponding to the positive mask elements X_ref : np.ndarray The (non-negative) array of reference or background elements. Must have the same shape as ``X``. power : number > 0 or np.inf If finite, returns the soft mask computed in a numerically stable way If infinite, returns a hard (binary) mask equivalent to ``X > X_ref``. Note: for hard masks, ties are always broken in favor of ``X_ref`` (``mask=0``). split_zeros : bool If `True`, entries where ``X`` and ``X_ref`` are both small (close to 0) will receive mask values of 0.5. Otherwise, the mask is set to 0 for these entries. Returns ------- mask : np.ndarray, shape=X.shape The output mask array Raises ------ ParameterError If ``X`` and ``X_ref`` have different shapes. If ``X`` or ``X_ref`` are negative anywhere If ``power <= 0`` Examples -------- >>> X = 2 * np.ones((3, 3)) >>> X_ref = np.vander(np.arange(3.0)) >>> X array([[ 2., 2., 2.], [ 2., 2., 2.], [ 2., 2., 2.]]) >>> X_ref array([[ 0., 0., 1.], [ 1., 1., 1.], [ 4., 2., 1.]]) >>> librosa.util.softmask(X, X_ref, power=1) array([[ 1. , 1. , 0.667], [ 0.667, 0.667, 0.667], [ 0.333, 0.5 , 0.667]]) >>> librosa.util.softmask(X_ref, X, power=1) array([[ 0. , 0. , 0.333], [ 0.333, 0.333, 0.333], [ 0.667, 0.5 , 0.333]]) >>> librosa.util.softmask(X, X_ref, power=2) array([[ 1. , 1. , 0.8], [ 0.8, 0.8, 0.8], [ 0.2, 0.5, 0.8]]) >>> librosa.util.softmask(X, X_ref, power=4) array([[ 1. , 1. , 0.941], [ 0.941, 0.941, 0.941], [ 0.059, 0.5 , 0.941]]) >>> librosa.util.softmask(X, X_ref, power=100) array([[ 1.000e+00, 1.000e+00, 1.000e+00], [ 1.000e+00, 1.000e+00, 1.000e+00], [ 7.889e-31, 5.000e-01, 1.000e+00]]) >>> librosa.util.softmask(X, X_ref, power=np.inf) array([[ True, True, True], [ True, True, True], [False, False, True]], dtype=bool) """ if X.shape != X_ref.shape: raise ParameterError(f"Shape mismatch: {X.shape}!={X_ref.shape}") if np.any(X < 0) or np.any(X_ref < 0): raise ParameterError("X and X_ref must be non-negative") if power <= 0: raise ParameterError("power must be strictly positive") # We're working with ints, cast to float. dtype = X.dtype if not np.issubdtype(dtype, np.floating): dtype = np.float32 # Re-scale the input arrays relative to the larger value Z = np.maximum(X, X_ref).astype(dtype) bad_idx = Z < np.finfo(dtype).tiny Z[bad_idx] = 1 # For finite power, compute the softmask mask: np.ndarray if np.isfinite(power): mask = (X / Z) ** power ref_mask = (X_ref / Z) ** power good_idx = ~bad_idx mask[good_idx] /= mask[good_idx] + ref_mask[good_idx] # Wherever energy is below energy in both inputs, split the mask if split_zeros: mask[bad_idx] = 0.5 else: mask[bad_idx] = 0.0 else: # Otherwise, compute the hard mask mask = X > X_ref return mask def tiny(x: complex | np.ndarray) -> np.floating[Any]: """Compute the tiny-value corresponding to an input's data type. This is the smallest "usable" number representable in ``x.dtype`` (e.g., float32). This is primarily useful for determining a threshold for numerical underflow in division or multiplication operations. Parameters ---------- x : number (real or complex) or np.ndarray The array to compute the tiny-value for. All that matters here is ``x.dtype`` Returns ------- tiny_value : float The smallest positive usable number for the type of ``x``. If ``x`` is integer-typed, then the tiny value for ``np.float32`` is returned instead. See Also -------- numpy.finfo Examples -------- For a standard double-precision floating point number: >>> librosa.util.tiny(1.0) 2.2250738585072014e-308 Or explicitly as double-precision >>> librosa.util.tiny(np.asarray(1e-5, dtype=np.float64)) 2.2250738585072014e-308 Or complex numbers >>> librosa.util.tiny(1j) 2.2250738585072014e-308 Single-precision floating point: >>> librosa.util.tiny(np.asarray(1e-5, dtype=np.float32)) 1.1754944e-38 Integer >>> librosa.util.tiny(5) 1.1754944e-38 """ # Make sure we have an array view x = np.asarray(x) # Only floating types generate a tiny if np.issubdtype(x.dtype, np.floating) or np.issubdtype( x.dtype, np.complexfloating ): dtype = x.dtype else: dtype = np.dtype(np.float32) tiny: np.floating[Any] = np.finfo(dtype).tiny return tiny def fill_off_diagonal(x: np.ndarray, *, radius: float, value: float = 0) -> None: """Set all array entries to ``value`` if they lie outside a constraint region. In this case, the constraint region is the Sakoe-Chiba band which runs with a fixed ``radius`` along the main diagonal. When ``x.shape[0] != x.shape[1]``, the radius will be expanded so that ``x[-1, -1] = 1`` always. ``x`` will be modified in place. Parameters ---------- x : np.ndarray [shape=(N, M)] Input matrix, will be modified in place. radius : float The band radius (1/2 of the width) will be ``int(radius*min(x.shape))`` value : float ``x[n, m] = value`` when ``(n, m)`` lies outside the band. Examples -------- >>> x = np.ones((8, 8)) >>> librosa.util.fill_off_diagonal(x, radius=0.25) >>> x array([[1, 1, 0, 0, 0, 0, 0, 0], [1, 1, 1, 0, 0, 0, 0, 0], [0, 1, 1, 1, 0, 0, 0, 0], [0, 0, 1, 1, 1, 0, 0, 0], [0, 0, 0, 1, 1, 1, 0, 0], [0, 0, 0, 0, 1, 1, 1, 0], [0, 0, 0, 0, 0, 1, 1, 1], [0, 0, 0, 0, 0, 0, 1, 1]]) >>> x = np.ones((8, 12)) >>> librosa.util.fill_off_diagonal(x, radius=0.25) >>> x array([[1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0], [0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0], [0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0], [0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0], [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1], [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1]]) """ nx, ny = x.shape # Calculate the radius in indices, rather than proportion radius = int(np.round(radius * np.min(x.shape))) nx, ny = x.shape offset = np.abs((x.shape[0] - x.shape[1])) if nx < ny: idx_u = np.triu_indices_from(x, k=radius + offset) idx_l = np.tril_indices_from(x, k=-radius) else: idx_u = np.triu_indices_from(x, k=radius) idx_l = np.tril_indices_from(x, k=-radius - offset) # modify input matrix x[idx_u] = value x[idx_l] = value def cyclic_gradient( data: np.ndarray, *, edge_order: Literal[1, 2] = 1, axis: int = -1 ) -> np.ndarray: """Estimate the gradient of a function over a uniformly sampled, periodic domain. This is essentially the same as `np.gradient`, except that edge effects are handled by wrapping the observations (i.e. assuming periodicity) rather than extrapolation. Parameters ---------- data : np.ndarray The function values observed at uniformly spaced positions on a periodic domain edge_order : {1, 2} The order of the difference approximation used for estimating the gradient axis : int The axis along which gradients are calculated. Returns ------- grad : np.ndarray like ``data`` The gradient of ``data`` taken along the specified axis. See Also -------- numpy.gradient Examples -------- This example estimates the gradient of cosine (-sine) from 64 samples using direct (aperiodic) and periodic gradient calculation. >>> import matplotlib.pyplot as plt >>> x = 2 * np.pi * np.linspace(0, 1, num=64, endpoint=False) >>> y = np.cos(x) >>> grad = np.gradient(y) >>> cyclic_grad = librosa.util.cyclic_gradient(y) >>> true_grad = -np.sin(x) * 2 * np.pi / len(x) >>> fig, ax = plt.subplots() >>> ax.plot(x, true_grad, label='True gradient', linewidth=5, ... alpha=0.35) >>> ax.plot(x, cyclic_grad, label='cyclic_gradient') >>> ax.plot(x, grad, label='np.gradient', linestyle=':') >>> ax.legend() >>> # Zoom into the first part of the sequence >>> ax.set(xlim=[0, np.pi/16], ylim=[-0.025, 0.025]) """ # Wrap-pad the data along the target axis by `edge_order` on each side padding = [(0, 0)] * data.ndim padding[axis] = (edge_order, edge_order) data_pad = np.pad(data, padding, mode="wrap") # Compute the gradient grad = np.gradient(data_pad, edge_order=edge_order, axis=axis) # Remove the padding slices = [slice(None)] * data.ndim slices[axis] = slice(edge_order, -edge_order) grad_slice: np.ndarray = grad[tuple(slices)] return grad_slice @numba.jit(nopython=True, cache=True) # type: ignore def __shear_dense(X: np.ndarray, *, factor: int = +1, axis: int = -1) -> np.ndarray: """Numba-accelerated shear for dense (ndarray) arrays""" if axis == 0: X = X.T X_shear = np.empty_like(X) for i in range(X.shape[1]): X_shear[:, i] = np.roll(X[:, i], factor * i) if axis == 0: X_shear = X_shear.T return X_shear if TYPE_CHECKING: def _asformat_sparse(X: _SparseArray | _SparseMatrix, fmt: str) -> _SparseArray | _SparseMatrix: ... else: def _asformat_sparse(X, fmt): return X.asformat(fmt) def __shear_sparse( X: _SparseArray | _SparseMatrix, *, factor: int = +1, axis: int = -1 ) -> _SparseArray | _SparseMatrix: """Fast shearing for sparse arrays/matrices Shearing is performed using CSC indices, and the result is converted back to whatever sparse format the data was originally provided in. """ if X.ndim != 2: raise ParameterError(f"Input must be 2D. Provided shape={X.shape}.") fmt = X.format is_matrix = isinstance(X, scipy.sparse.spmatrix) # If axis==0, operate on the transpose, but don't reassign X (avoids mypy ignore) X_in = X.T if axis == 0 else X # Now we're definitely rolling on the correct axis, and definitely CSC X_shear: scipy.sparse.csc_array | scipy.sparse.csc_matrix if is_matrix: X_shear = scipy.sparse.csc_matrix(X_in, copy=True) else: X_shear = scipy.sparse.csc_array(X_in, copy=True) # The idea here is to repeat the shear amount (factor * range) # by the number of non-zeros for each column. # The number of non-zeros is computed by diffing the index pointer array roll = np.repeat(factor * np.arange(X_shear.shape[1]), np.diff(X_shear.indptr)) # In-place roll np.mod(X_shear.indices + roll, X_shear.shape[0], out=X_shear.indices) if axis == 0: # Undo the transpose; normalize back to CSC for consistent downstream typing if is_matrix: X_shear = scipy.sparse.csc_matrix(X_shear.T) else: X_shear = scipy.sparse.csc_array(X_shear.T) # And convert back to the input format return _asformat_sparse(X_shear, fmt) @overload def shear(X: np.ndarray, *, factor: int = ..., axis: int = ...) -> np.ndarray: ... @overload def shear(X: _SparseArray | _SparseMatrix, *, factor: int = ..., axis: int = ...) -> _SparseArray | _SparseMatrix: ... def shear( X: np.ndarray | _SparseArray | _SparseMatrix, *, factor: int = 1, axis: int = -1 ) -> np.ndarray | _SparseArray | _SparseMatrix: """Shear an array (or matrix) by a given factor. The column ``X[:, n]`` will be displaced (rolled) by ``factor * n`` This is primarily useful for converting between lag and recurrence representations: shearing with ``factor=-1`` converts the main diagonal to a horizontal. Shearing with ``factor=1`` converts a horizontal to a diagonal. Parameters ---------- X : np.ndarray [ndim=2] or scipy.sparse array/matrix The array/matrix to be sheared factor : int The shear factor: ``X[:, n] -> np.roll(X[:, n], factor * n)`` axis : int The axis along which to shear Returns ------- X_shear : same type as ``X`` The sheared matrix or array Examples -------- >>> E = np.eye(3) >>> librosa.util.shear(E, factor=-1, axis=-1) array([[1., 1., 1.], [0., 0., 0.], [0., 0., 0.]]) >>> librosa.util.shear(E, factor=-1, axis=0) array([[1., 0., 0.], [1., 0., 0.], [1., 0., 0.]]) >>> librosa.util.shear(E, factor=1, axis=-1) array([[1., 0., 0.], [0., 0., 1.], [0., 1., 0.]]) """ if not np.issubdtype(type(factor), np.integer): raise ParameterError(f"factor={factor} must be integer-valued") # Suppress type checks because mypy doesn't like numba jitting # or scipy sparse conversion if scipy.sparse.issparse(X): return __shear_sparse(X, factor=factor, axis=axis) else: return __shear_dense(X, factor=factor, axis=axis) # type: ignore def stack(arrays: list[np.ndarray], *, axis: int = 0) -> np.ndarray: """Stack one or more arrays along a target axis. This function is similar to `np.stack`, except that memory contiguity is retained when stacking along the first dimension. This is useful when combining multiple monophonic audio signals into a multi-channel signal, or when stacking multiple feature representations to form a multi-dimensional array. Parameters ---------- arrays : list one or more `np.ndarray` axis : int The target axis along which to stack. ``axis=0`` creates a new first axis, and ``axis=-1`` creates a new last axis. Returns ------- arr_stack : np.ndarray [shape=(len(arrays), array_shape) or shape=(array_shape, len(arrays))] The input arrays, stacked along the target dimension. If ``axis=0``, then ``arr_stack`` will be F-contiguous. Otherwise, ``arr_stack`` will be C-contiguous by default, as computed by `np.stack`. Raises ------ ParameterError - If ``arrays`` do not all have the same shape - If no ``arrays`` are given See Also -------- numpy.stack numpy.ndarray.flags frame Examples -------- Combine two buffers into a contiguous arrays >>> y_left = np.ones(5) >>> y_right = -np.ones(5) >>> y_stereo = librosa.util.stack([y_left, y_right], axis=0) >>> y_stereo array([[ 1., 1., 1., 1., 1.], [-1., -1., -1., -1., -1.]]) >>> y_stereo.flags C_CONTIGUOUS : False F_CONTIGUOUS : True OWNDATA : True WRITEABLE : True ALIGNED : True WRITEBACKIFCOPY : False UPDATEIFCOPY : False Or along the trailing axis >>> y_stereo = librosa.util.stack([y_left, y_right], axis=-1) >>> y_stereo array([[ 1., -1.], [ 1., -1.], [ 1., -1.], [ 1., -1.], [ 1., -1.]]) >>> y_stereo.flags C_CONTIGUOUS : True F_CONTIGUOUS : False OWNDATA : True WRITEABLE : True ALIGNED : True WRITEBACKIFCOPY : False UPDATEIFCOPY : False """ shapes = {arr.shape for arr in arrays} if len(shapes) > 1: raise ParameterError("all input arrays must have the same shape") elif len(shapes) < 1: raise ParameterError("at least one input array must be provided for stack") shape_in = shapes.pop() if axis != 0: return np.stack(arrays, axis=axis) else: # If axis is 0, enforce F-ordering shape = tuple([len(arrays), *list(shape_in)]) # Find the common dtype for all inputs dtype = np.result_type(*arrays) # Allocate an empty array of the right shape and type result = np.empty(shape, dtype=dtype, order="F") # Stack into the preallocated buffer np.stack(arrays, axis=axis, out=result) return result def dtype_r2c( d: DTypeLike, *, default: type[np.complexfloating] = np.complex64 ) -> np.dtype[np.complexfloating]: """Find the complex numpy dtype corresponding to a real dtype. This is used to maintain numerical precision and memory footprint when constructing complex arrays from real-valued data (e.g. in a Fourier transform). A `float32` (single-precision) type maps to `complex64`, while a `float64` (double-precision) maps to `complex128`. Parameters ---------- d : np.dtype The real-valued dtype to convert to complex. If ``d`` is a complex type already, it will be returned. default : np.dtype, optional The default complex target type, if ``d`` does not match a known dtype Returns ------- d_c : np.dtype The complex dtype See Also -------- dtype_c2r numpy.dtype Examples -------- >>> librosa.util.dtype_r2c(np.float32) dtype('complex64') >>> librosa.util.dtype_r2c(np.int16) dtype('complex64') >>> librosa.util.dtype_r2c(np.complex128) dtype('complex128') """ mapping: dict[np.dtype[np.floating], type[np.complexfloating]] = { np.dtype(np.float32): np.complex64, np.dtype(np.float64): np.complex128, } # If we're given a complex type already, return it dt: np.dtype = np.dtype(d) if dt.kind == "c": return dt # Otherwise, try to map the dtype. # If no match is found, return the default. return np.dtype(mapping.get(dt, default)) def dtype_c2r( d: DTypeLike, *, default: type[np.floating] | None = np.float32 ) -> np.dtype[np.floating]: """Find the real numpy dtype corresponding to a complex dtype. This is used to maintain numerical precision and memory footprint when constructing real arrays from complex-valued data (e.g. in an inverse Fourier transform). A `complex64` (single-precision) type maps to `float32`, while a `complex128` (double-precision) maps to `float64`. Parameters ---------- d : np.dtype The complex-valued dtype to convert to real. If ``d`` is a real (float) type already, it will be returned. default : np.dtype, optional The default real target type, if ``d`` does not match a known dtype Returns ------- d_r : np.dtype The real dtype See Also -------- dtype_r2c numpy.dtype Examples -------- >>> librosa.util.dtype_r2c(np.complex64) dtype('float32') >>> librosa.util.dtype_r2c(np.float32) dtype('float32') >>> librosa.util.dtype_r2c(np.int16) dtype('float32') >>> librosa.util.dtype_r2c(np.complex128) dtype('float64') """ mapping: dict[np.dtype[np.complexfloating], type[np.floating]] = { np.dtype(np.complex64): np.float32, np.dtype(np.complex128): np.float64, } # If we're given a real type already, return it dt: np.dtype = np.dtype(d) if dt.kind == "f": return dt # Otherwise, try to map the dtype. # If no match is found, return the default. return np.dtype(mapping.get(dt, default)) @numba.jit(nopython=True, cache=True) def __count_unique(x): """Count the number of unique values in an array. This function is a helper for `count_unique` and is not to be called directly. """ uniques = np.unique(x) return uniques.shape[0] def count_unique(data: np.ndarray, *, axis: int = -1) -> NDArray[np.int_]: """Count the number of unique values along a given axis. Parameters ---------- data : np.ndarray The input array axis : int The target axis to count Returns ------- n_uniques The number of unique values. This array will have one fewer dimension than the input. See Also -------- is_unique Examples -------- >>> x = np.vander(np.arange(5)) >>> x array([[ 0, 0, 0, 0, 1], [ 1, 1, 1, 1, 1], [ 16, 8, 4, 2, 1], [ 81, 27, 9, 3, 1], [256, 64, 16, 4, 1]]) >>> # Count unique values along rows (within columns) >>> librosa.util.count_unique(x, axis=0) array([5, 5, 5, 5, 1]) >>> # Count unique values along columns (within rows) >>> librosa.util.count_unique(x, axis=-1) array([2, 1, 5, 5, 5]) """ return np.apply_along_axis(__count_unique, axis, data) @numba.jit(nopython=True, cache=True) def __is_unique(x): """Determine if the input array has all unique values. This function is a helper for `is_unique` and is not to be called directly. """ uniques = np.unique(x) return uniques.shape[0] == x.size def is_unique(data: np.ndarray, *, axis: int = -1) -> NDArray[np.bool]: """Determine if the input consists of all unique values along a given axis. Parameters ---------- data : np.ndarray The input array axis : int The target axis Returns ------- is_unique Array of booleans indicating whether the data is unique along the chosen axis. This array will have one fewer dimension than the input. See Also -------- count_unique Examples -------- >>> x = np.vander(np.arange(5)) >>> x array([[ 0, 0, 0, 0, 1], [ 1, 1, 1, 1, 1], [ 16, 8, 4, 2, 1], [ 81, 27, 9, 3, 1], [256, 64, 16, 4, 1]]) >>> # Check uniqueness along rows >>> librosa.util.is_unique(x, axis=0) array([ True, True, True, True, False]) >>> # Check uniqueness along columns >>> librosa.util.is_unique(x, axis=-1) array([False, False, True, True, True]) """ return np.apply_along_axis(__is_unique, axis, data) @numba.vectorize( nopython=True, cache=True, identity=0 ) # type: ignore def _cabs2(x: _ComplexLike_co) -> _FloatLike_co: # pragma: no cover """Efficiently compute abs2 on complex inputs""" return x.real**2 + x.imag**2 @overload def abs2(x: np.ndarray, dtype: DTypeLike | None = ...) -> np.ndarray: ... @overload def abs2(x: _Complex, dtype: DTypeLike | None = ...) -> np.floating: ... def abs2( x: np.ndarray | _Real | _Complex, dtype: DTypeLike | None = None ) -> np.ndarray | np.floating: """Compute the squared magnitude of a real or complex array. This function is equivalent to calling `np.abs(x)**2` but it is slightly more efficient. Parameters ---------- x : np.ndarray or scalar, real or complex typed The input data, either real (float32, float64) or complex (complex64, complex128) typed dtype : np.dtype, optional The data type of the output array. If not provided, it will be inferred from `x` Returns ------- p : np.ndarray or scale, real squared magnitude of `x` Examples -------- >>> librosa.util.abs2(3 + 4j) 25.0 >>> librosa.util.abs2((0.5j)**np.arange(8)) array([1.000e+00, 2.500e-01, 6.250e-02, 1.562e-02, 3.906e-03, 9.766e-04, 2.441e-04, 6.104e-05]) """ if np.iscomplexobj(x): # suppress type check, mypy doesn't like vectorization if dtype is None: dtype = dtype_c2r(np.asarray(x).dtype) y: np.ndarray = np.empty_like(x, dtype=dtype) _cabs2(x, y) return y else: # suppress type check, mypy doesn't know this is real return np.square(x, dtype=dtype) @numba.vectorize( nopython=True, cache=True, identity=1 ) # type: ignore def _phasor_angles(x) -> np.complexfloating: return np.cos(x) + 1j * np.sin(x) # type: ignore @overload def phasor(angles: np.ndarray, *, mag: np.ndarray | None = ...) -> np.ndarray: ... @overload def phasor(angles: _Real, *, mag: _Number | None = ...) -> np.complexfloating: ... def phasor( angles: np.ndarray | _Real, *, mag: np.ndarray | _Number | None = None, ) -> np.ndarray | np.complexfloating: """Construct a complex phasor representation from angles. When `mag` is not provided, this is equivalent to: z = np.cos(angles) + 1j * np.sin(angles) or by Euler's formula: z = np.exp(1j * angles) When `mag` is provided, this is equivalent to: z = mag * np.exp(1j * angles) This function should be more efficient (in time and memory) than the equivalent' formulations above, but produce numerically identical results. Parameters ---------- angles : np.ndarray or scalar, real-valued Angle(s), measured in radians mag : np.ndarray or scalar, optional If provided, phasor(s) will be scaled by `mag`. If not provided (default), phasors will have unit magnitude. `mag` must be of compatible shape to multiply with `angles`. Returns ------- z : np.ndarray or scalar, complex-valued Complex number(s) z corresponding to the given angle(s) and optional magnitude(s). Examples -------- Construct unit phasors at angles 0, pi/2, and pi: >>> librosa.util.phasor([0, np.pi/2, np.pi]) array([ 1.000e+00+0.000e+00j, 6.123e-17+1.000e+00j, -1.000e+00+1.225e-16j]) Construct a phasor with magnitude 1/2: >>> librosa.util.phasor(np.pi/2, mag=0.5) (3.061616997868383e-17+0.5j) Or arrays of angles and magnitudes: >>> librosa.util.phasor(np.array([0, np.pi/2]), mag=np.array([0.5, 1.5])) array([5.000e-01+0.j , 9.185e-17+1.5j]) """ z = np.empty_like(angles, dtype=dtype_r2c(np.asarray(angles).dtype)) _phasor_angles(angles, z) if mag is not None: z *= mag return z[()] @overload def interp_broadcast( *, x1: np.ndarray, x1_pos: np.ndarray, x2: np.ndarray, x2_pos: np.ndarray, interp_pos: np.ndarray | None = None, op: None, kind: _InterpKind = "linear", fill_value: float = 0, axis: int = -2, ) -> tuple[NDArray[np.float64], NDArray[np.float64]]: ... @overload def interp_broadcast( *, x1: np.ndarray, x1_pos: np.ndarray, x2: np.ndarray, x2_pos: np.ndarray, interp_pos: np.ndarray | None = None, op: Callable[[np.ndarray, np.ndarray], np.ndarray] = np.multiply, kind: _InterpKind = "linear", fill_value: float = 0, axis: int = -2, ) -> NDArray[np.float64]: ... def interp_broadcast( *, x1: np.ndarray, x1_pos: np.ndarray, x2: np.ndarray, x2_pos: np.ndarray, interp_pos: np.ndarray | None = None, op: Callable[[np.ndarray, np.ndarray], np.ndarray] | None = np.multiply, kind: _InterpKind = "linear", fill_value: float = 0, axis: int = -2, ) -> NDArray[np.float64] | tuple[NDArray[np.float64], NDArray[np.float64]]: """Broadcast two arrays using interpolation Interpolates two arrays along a given axis to a common grid, and performs a broadcast operation (eg. ``np.multiply``) to combine them. It is useful for retrieving the DFT / AC product [1]_ and the Fundamental Tempogram [2]_. .. [1] Peeters, G. "Spectral and Temporal Periodicity Representations of Rhythm for the Automatic Classification of Music Audio Signal." In IEEE Transactions on Audio, Speech, and Language Processing, vol. 19, no. 5, pp. 1242--1252, July 2011. .. [2] Cozens, James, and Simon Godsill. "Dynamic Time Signature Recognition, Tempo Inference, and Beat Tracking Through the Metrogram Transform." In IEEE Open Journal of Signal Processing, pp. 1--9, 2023. Parameters ---------- x1 : np.ndarray An array with broadcast compatible dimensions (except along the axis of interpolation) with ``x2``. x1_pos : np.ndarray Positioning data along the axis of interpolation for ``x1``. x2 : np.ndarray An array with broadcast compatible dimensions (except along the axis of interpolation) with ``x1``. x2_pos : np.ndarray Positioning data along the axis of interpolation for ``x2``. interp_pos : np.ndarray Positioning data for the interpolation grid. Default: ``x1_pos``. op : function [optional] A broadcast operation performed on the two interpolated arrays. Default: ``np.multiply``. kind : str Interpolation type. See ``scipy.interpolate.interp1d``. Default: ``"linear"`` fill_value : float The value to fill when extrapolating beyond the observed range. Default: ``0`` axis : int The axis of interpolation. Default: ``-2`` Returns ------- result : np.ndarray or (np.ndarray, np.ndarray) The result from combining both arrays after interpolation. If ``op`` is set to ``None``, returns the interpolated arrays separately ``(y1, y2)``. Examples -------- >>> import numpy as np >>> >>> # two arrays of different lengths and sampling positions >>> x1 = np.array([1, 1, 1]) >>> x1_pos = np.array([0, 0.5, 1]) >>> x2 = np.array([5, 10]) >>> x2_pos = np.array([0, 1]) >>> >>> # interpolate to x1_pos and broadcast multiply (the defaults) >>> product = librosa.util.interp_broadcast( ... x1=x1, ... x1_pos=x1_pos, ... x2=x2, ... x2_pos=x2_pos, ... axis=0, ... ) >>> >>> product array([ 5. , 7.5, 10. ]) See Also -------- librosa.feature.metrogram """ if interp_pos is None: interp_pos = x1_pos min_ndim = min(x1.ndim, x2.ndim) if axis < -min_ndim or axis >= min_ndim: raise ParameterError( f"axis={axis} is out of range for minimum ndim={min_ndim}" ) import scipy.interpolate x1_interp = scipy.interpolate.interp1d( x1_pos, x1, axis=axis, kind=kind, copy=False, bounds_error=False, fill_value=fill_value ) x2_interp = scipy.interpolate.interp1d( x2_pos, x2, axis=axis, kind=kind, copy=False, bounds_error=False, fill_value=fill_value ) y1 = cast("NDArray[np.float64]", x1_interp(interp_pos)) y2 = cast("NDArray[np.float64]", x2_interp(interp_pos)) if op is None: return y1, y2 try: np.broadcast_shapes(y1.shape, y2.shape) except ValueError as exc: raise ParameterError( f"Interpolating x1.shape={x1.shape} and x2.shape={x2.shape} along " f"axis={axis} leads to y1.shape={y1.shape} and y2.shape={y2.shape}, " "which are not broadcast compatible." ) from exc return op(y1, y2) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1786462635.214252 librosa-1.0.0/librosa/version.py0000644000175100017510000000330315236640653016314 0ustar00runnerrunner#!/usr/bin/env python # -*- coding: utf-8 -*- """Version info""" import importlib import sys short_version = "1.0.0" version = "1.0.0" def __get_mod_version(modname): try: if modname in sys.modules: mod = sys.modules[modname] else: mod = importlib.import_module(modname) try: return mod.__version__ except AttributeError: return "installed, no version number available" except ImportError: return None def show_versions() -> None: """Return the version information for all librosa dependencies.""" core_deps = [ "numpy", "scipy", "sklearn", "joblib", "decorator", "numba", "soundfile", "pooch", "soxr", "lazy_loader", "msgpack", ] extra_deps = [ "numpydoc", "sphinx", "pydata-sphinx-theme", "matplotlib", "sphinx_gallery", "sphinx-design", "sphinxcontrib-googleanalytics", "sphinx-copybutton", "umap-learn", "pandas", "myst-parser", "mir_eval", "ipython", "sphinxcontrib.rsvgconverter", "pytest", "pytest_mpl", "pytest_cov", "samplerate", "resampy", "presets", "packaging", "scipy-stubs", "types-decorator", ] print("INSTALLED VERSIONS") print("------------------") print(f"python: {sys.version}\n") print(f"librosa: {version}\n") for dep in core_deps: print("{}: {}".format(dep, __get_mod_version(dep))) print("") for dep in extra_deps: print("{}: {}".format(dep, __get_mod_version(dep))) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462640.9806178 librosa-1.0.0/librosa.egg-info/0000755000175100017510000000000015236640661015747 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1786462640.950318 librosa-1.0.0/librosa.egg-info/PKG-INFO0000644000175100017510000001505315236640661017050 0ustar00runnerrunnerMetadata-Version: 2.4 Name: librosa Version: 1.0.0 Summary: Python module for audio and music processing Home-page: https://librosa.org Author: Brian McFee, librosa development team Author-email: brian.mcfee@nyu.edu License: ISC Project-URL: Documentation, https://librosa.org/doc Project-URL: Download, https://github.com/librosa/librosa/releases Project-URL: Source, https://github.com/librosa/librosa Project-URL: Tracker, https://github.com/librosa/librosa/issues Project-URL: Discussion forum, https://groups.google.com/g/librosa Classifier: License :: OSI Approved :: ISC License (ISCL) Classifier: Programming Language :: Python Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis Classifier: Framework :: Matplotlib Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.14 Requires-Python: >=3.12 Description-Content-Type: text/markdown; charset=UTF-8 License-File: LICENSE.md Requires-Dist: numba>=0.61.0 Requires-Dist: numpy>=2.1.0 Requires-Dist: scipy>=1.15.0 Requires-Dist: scikit-learn>=1.6.0 Requires-Dist: joblib>=1.2 Requires-Dist: decorator>=5.2.1 Requires-Dist: soundfile>=0.12.1 Requires-Dist: pooch>=1.7 Requires-Dist: soxr>=1.0.0 Requires-Dist: lazy_loader>=0.3 Requires-Dist: msgpack>=1.0.5 Provides-Extra: docs Requires-Dist: numpydoc>=1.8.0; extra == "docs" Requires-Dist: sphinx!=1.3.1; extra == "docs" Requires-Dist: matplotlib>=3.10.0; extra == "docs" Requires-Dist: pydata-sphinx-theme>=0.18.0; extra == "docs" Requires-Dist: sphinx-gallery>=0.7; extra == "docs" Requires-Dist: sphinx-design>=0.7.0; extra == "docs" Requires-Dist: mir_eval>=0.5; extra == "docs" Requires-Dist: ipython>=7.0; extra == "docs" Requires-Dist: sphinxcontrib-svg2pdfconverter; extra == "docs" Requires-Dist: sphinxcontrib-googleanalytics>=0.4; extra == "docs" Requires-Dist: presets; extra == "docs" Requires-Dist: sphinx-copybutton>=0.5.2; extra == "docs" Requires-Dist: myst-parser>=2.0; extra == "docs" Requires-Dist: pandas>=2.0; extra == "docs" Requires-Dist: umap-learn>=0.5.4; extra == "docs" Provides-Extra: tests Requires-Dist: matplotlib>=3.10.0; extra == "tests" Requires-Dist: packaging>=23.2; extra == "tests" Requires-Dist: pytest-mpl; extra == "tests" Requires-Dist: pytest-cov; extra == "tests" Requires-Dist: pytest; extra == "tests" Requires-Dist: samplerate; extra == "tests" Requires-Dist: resampy>=0.4.3; extra == "tests" Requires-Dist: scipy-stubs; extra == "tests" Requires-Dist: types-decorator; extra == "tests" Provides-Extra: display Requires-Dist: matplotlib>=3.10.0; extra == "display" Dynamic: license-file [![librosa logo](docs/img/librosa_logo_text.svg)](https://librosa.org/) # librosa `librosa` is a Python library for audio and music signal processing. It provides the foundational algorithms and tools required for building music information retrieval (MIR) systems. [![PyPI](https://img.shields.io/pypi/v/librosa.svg)](https://pypi.python.org/pypi/librosa) [![Anaconda-Server Badge](https://anaconda.org/conda-forge/librosa/badges/version.svg)](https://anaconda.org/conda-forge/librosa) [![License](https://img.shields.io/pypi/l/librosa.svg)](https://github.com/librosa/librosa/blob/main/LICENSE.md) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.591533.svg)](https://doi.org/10.5281/zenodo.591533) [![EffVer Versioning](https://img.shields.io/badge/version_scheme-EffVer-0097a7)](https://jacobtomlinson.dev/effver) [![Docs](https://github.com/librosa/librosa/actions/workflows/docs.yml/badge.svg)](https://librosa.org/doc/latest/index.html) [![Scientific Python Ecosystem Coordination](https://img.shields.io/badge/SPEC-0,1,7-green?labelColor=%23004811&color=%235CA038)](https://scientific-python.org/specs/) [![CI](https://github.com/librosa/librosa/actions/workflows/ci.yml/badge.svg)](https://github.com/librosa/librosa/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/librosa/librosa/branch/main/graph/badge.svg?token=ULWnUHaIJC)](https://codecov.io/gh/librosa/librosa) ## Documentation Full documentation is available at: * https://librosa.org/doc/ If you're new to librosa, we recommend starting with the [tutorials](https://librosa.org/doc/latest/auto_tutorials). If you're looking for API details, head directly to the [API reference](https://librosa.org/doc/latest/api). To see what's new, check out the [change log](https://librosa.org/doc/latest/changelog.html). ## Installation ### Using PyPI The latest stable release is available on PyPI, and you can install it by the command ``` pip install librosa ``` ### Using conda Anaconda users can install using `conda-forge`: ``` conda install -c conda-forge librosa ``` For optional dependencies and advanced setup, see the guide in the documentation. --- ## Citing There are two ways to cite librosa in scholarly work, depending on whether you are citing it for its use in your work or for its design and motivation. We additionally encourage you to cite the original publications describing the methods implemented in librosa, in addition to citing librosa itself. References can often be found within function documentation. ### Citing for use If you are citing for use in your own work, please cite the version you used by retrieving the appropriate DOI and citation information from Zenodo. [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.591533.svg)](https://doi.org/10.5281/zenodo.591533) This ensures that all contributing authors are credited for the work you are building on. From librosa version 0.10.2 or later, you can also use `librosa.cite()` to get the DOI link for any version of librosa: ```python import librosa librosa.cite() ``` And from 1.0 onward, you can directly retrieve the BibTeX entry for the version you are using: ```python print(librosa.cite(bib=True)) ``` ### Citing for design If you wish to cite librosa for its design, motivation, etc., please cite the paper published at SciPy 2015: McFee, Brian, Colin Raffel, Dawen Liang, Daniel PW Ellis, Matt McVicar, Eric Battenberg, and Oriol Nieto. "librosa: Audio and music signal analysis in python." In Proceedings of the 14th python in science conference, pp. 18-25. 2015. As a rule of thumb: - Use a versioned librosa citation to document the software implementation used in your experiments. - Cite the original methodological papers for the algorithms that are central to your work. - Cite the SciPy paper when discussing librosa itself, its design, or its contribution as a software library. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1786462640.970537 librosa-1.0.0/librosa.egg-info/SOURCES.txt0000644000175100017510000000333215236640661017634 0ustar00runnerrunnerAUTHORS.md CODE_OF_CONDUCT.md CONTRIBUTING.md LICENSE.md MANIFEST.in README.md pyproject.toml setup.cfg setup.py librosa/__init__.py librosa/__init__.pyi librosa/_cache.py librosa/_typing.py librosa/beat.py librosa/decompose.py librosa/display.py librosa/effects.py librosa/filters.py librosa/onset.py librosa/py.typed librosa/segment.py librosa/sequence.py librosa/version.py librosa.egg-info/PKG-INFO librosa.egg-info/SOURCES.txt librosa.egg-info/dependency_links.txt librosa.egg-info/not-zip-safe librosa.egg-info/requires.txt librosa.egg-info/top_level.txt librosa/core/__init__.py librosa/core/__init__.pyi librosa/core/audio.py librosa/core/constantq.py librosa/core/convert.py librosa/core/harmonic.py librosa/core/intervals.msgpack librosa/core/intervals.py librosa/core/notation.py librosa/core/pitch.py librosa/core/spectrum.py librosa/feature/__init__.py librosa/feature/__init__.pyi librosa/feature/inverse.py librosa/feature/rhythm.py librosa/feature/spectral.py librosa/feature/utils.py librosa/util/__init__.py librosa/util/__init__.pyi librosa/util/_nnls.py librosa/util/decorators.py librosa/util/deprecation.py librosa/util/exceptions.py librosa/util/files.py librosa/util/matching.py librosa/util/utils.py librosa/util/example_data/__init__.py librosa/util/example_data/index.json librosa/util/example_data/registry.txt tests/test_beat.py tests/test_cache.py tests/test_constantq.py tests/test_convert.py tests/test_core.py tests/test_decompose.py tests/test_display.py tests/test_dtw.py tests/test_effects.py tests/test_failures.py tests/test_features.py tests/test_filters.py tests/test_intervals.py tests/test_multichannel.py tests/test_notation.py tests/test_onset.py tests/test_segment.py tests/test_sequence.py tests/test_util.py././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1786462640.951827 librosa-1.0.0/librosa.egg-info/dependency_links.txt0000644000175100017510000000000115236640661022015 0ustar00runnerrunner ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462640.7687764 librosa-1.0.0/librosa.egg-info/not-zip-safe0000644000175100017510000000000115236640661020175 0ustar00runnerrunner ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462640.9541194 librosa-1.0.0/librosa.egg-info/requires.txt0000644000175100017510000000115315236640661020347 0ustar00runnerrunnernumba>=0.61.0 numpy>=2.1.0 scipy>=1.15.0 scikit-learn>=1.6.0 joblib>=1.2 decorator>=5.2.1 soundfile>=0.12.1 pooch>=1.7 soxr>=1.0.0 lazy_loader>=0.3 msgpack>=1.0.5 [display] matplotlib>=3.10.0 [docs] numpydoc>=1.8.0 sphinx!=1.3.1 matplotlib>=3.10.0 pydata-sphinx-theme>=0.18.0 sphinx-gallery>=0.7 sphinx-design>=0.7.0 mir_eval>=0.5 ipython>=7.0 sphinxcontrib-svg2pdfconverter sphinxcontrib-googleanalytics>=0.4 presets sphinx-copybutton>=0.5.2 myst-parser>=2.0 pandas>=2.0 umap-learn>=0.5.4 [tests] matplotlib>=3.10.0 packaging>=23.2 pytest-mpl pytest-cov pytest samplerate resampy>=0.4.3 scipy-stubs types-decorator ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462640.9543226 librosa-1.0.0/librosa.egg-info/top_level.txt0000644000175100017510000000001015236640661020470 0ustar00runnerrunnerlibrosa ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1786462635.214252 librosa-1.0.0/pyproject.toml0000644000175100017510000000630315236640653015541 0ustar00runnerrunner[build-system] requires = [ "setuptools >= 48", "wheel >= 0.29.0", ] build-backend = 'setuptools.build_meta' [tool.ruff] line-length = 119 extend-exclude = [ "docs/_build", "docs/auto_*", ] [tool.ruff.lint] select = ["E", "F", "S", "D", "W", "I", "PYI", "Q", "B", "SIM", "RUF", "TC", "NPY"] ignore = [ "D107", # This only affects inherited classes for us "D203", # Blank line before class docstring is silly "D205", # Blank line after summary is silly "D212", # Multi-line docstring summary is unnecessary "D213", # Multi-line docstring summary should end with a period, but we don't care "D400", # First line should end with a period, but we don't care "D402", # Docstring should not be a reST directive, but we don't care "D413", # Missing blank line after last section is actually more readable in our codebase "D415", # First line punctuation is not that important "D416", # Section name colon is not compatible with numpydoc "D417", # Missing argument descriptions are okay sometimes "S101", # Sometimes we need asserts for type checking to work "SIM108", # Ternary operators are bad for readability "SIM300", # Yoda condition is different in numpy vectorized code "RUF022", # We actually like having control over __all__ ordering ] [tool.ruff.lint.pydocstyle] convention = "numpy" [tool.ruff.lint.per-file-ignores] "**/__init__.py" = ["F401"] "**/__init__.pyi" = ["F401"] [tool.ruff.lint.flake8-bugbear] extend-immutable-calls = ["librosa.util.deprecation.Deprecated"] [tool.numpydoc_validation] checks = [ "all", "ES01", # No extended summary "EX01", # No examples section "SA01", # No See Also section "SA02", # Punctuation in See Also section is not that important "GL01", # Docstring starting on the same line as open quotes? "PR08", # Capitalization of parameter descriptions is not that important "PR09", # Punctuation of parameter descriptions is not that important "RT02", # It's useful to include shape descriptions in return types "SA04", # See Also descriptions are not that important "RT04", # Capitalization in return descriptions is not that important "RT05", # Punctuation in return descriptions is not that important "SS03", # Punctuation in summary is not that important "GL08", # @overload stubs and internal helpers do not need docstrings "SS01", # Module-level docstrings use autosummary format, not numpydoc "PR02", # We can have extra parameter names included, eg when using **kwargs ] exclude = [ # Exclude module-level docstrings (autosummary format, not numpydoc): "^[^.]+$", # Exclude all private functions and classes (those beginning with _): "\\._", ] exclude_files = [ '.*/__init__.py', '.*/version.py', '.*/_typing.py', '.*/_cache.py', ] [tool.ruff.lint.extend-per-file-ignores] "docs/**/*.py" = [ "E402", # Allow imports in the middle of the file "E501", # Ignore line length "I", # Disable isort to maintain your custom pedagogical import ordering "B018", # Allow "useless expressions" (necessary for Sphinx-Gallery to render repr outputs) "D", # Disable strict pydocstyle checks for tutorial scripts ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462640.9919045 librosa-1.0.0/setup.cfg0000644000175100017510000000565115236640661014452 0ustar00runnerrunner[tool:pytest] xfail_strict = true addopts = --cov-report term-missing --cov librosa --cov-report=xml --mpl --mpl-baseline-path=tests/baseline_images/test_display filterwarnings = ignore::DeprecationWarning:resampy.* [metadata] name = librosa version = attr: librosa.version.version description = Python module for audio and music processing long_description = file: README.md long_description_content_type = text/markdown; charset=UTF-8 url = https://librosa.org author = Brian McFee, librosa development team author_email = brian.mcfee@nyu.edu license = ISC license_file = LICENSE.md license_file_content_type = text/markdown; charset=UTF-8 project_urls = Documentation = https://librosa.org/doc Download = https://github.com/librosa/librosa/releases Source = https://github.com/librosa/librosa Tracker = https://github.com/librosa/librosa/issues Discussion forum = https://groups.google.com/g/librosa classifiers = License :: OSI Approved :: ISC License (ISCL) Programming Language :: Python Development Status :: 5 - Production/Stable Intended Audience :: Developers Topic :: Multimedia :: Sound/Audio :: Analysis Framework :: Matplotlib Programming Language :: Python :: 3 Programming Language :: Python :: 3.12 Programming Language :: Python :: 3.13 Programming Language :: Python :: 3.14 [options] packages = find: include_package_data = True install_requires = numba >= 0.61.0 numpy >= 2.1.0 scipy >= 1.15.0 scikit-learn >= 1.6.0 joblib >= 1.2 decorator >= 5.2.1 soundfile >= 0.12.1 pooch >= 1.7 soxr >= 1.0.0 lazy_loader >= 0.3 msgpack >= 1.0.5 python_requires = >=3.12 zip_safe = False [options.package_data] librosa = util/example_data/* py.typed core/intervals.msgpack [options.extras_require] docs = numpydoc >= 1.8.0 sphinx != 1.3.1 matplotlib >= 3.10.0 pydata-sphinx-theme >= 0.18.0 sphinx-gallery >= 0.7 sphinx-design >= 0.7.0 mir_eval >= 0.5 ipython >= 7.0 sphinxcontrib-svg2pdfconverter sphinxcontrib-googleanalytics >= 0.4 presets sphinx-copybutton >= 0.5.2 myst-parser >= 2.0 pandas >= 2.0 umap-learn >= 0.5.4 tests = matplotlib >= 3.10.0 packaging >= 23.2 pytest-mpl pytest-cov pytest samplerate resampy >= 0.4.3 scipy-stubs types-decorator display = matplotlib >= 3.10.0 [mypy] check_untyped_defs = True allow_redefinition = True follow_imports = silent strict = True disallow_untyped_defs = False disallow_incomplete_defs = False disallow_untyped_calls = False disallow_any_generics = False disallow_subclassing_any = False [mypy-soundfile.*] ignore_missing_imports = True [mypy-sklearn.*] ignore_missing_imports = True [mypy-numba.*] ignore_missing_imports = True [mypy-joblib.*] ignore_missing_imports = True [mypy-pooch.*] ignore_missing_imports = True [mypy-lazy_loader.*] ignore_missing_imports = True [mypy-soxr.*] ignore_missing_imports = True [mypy-msgpack.*] ignore_missing_imports = True [mypy-packaging.*] ignore_missing_imports = True [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2150636 librosa-1.0.0/setup.py0000644000175100017510000000010515236640653014331 0ustar00runnerrunnerfrom setuptools import setup if __name__ == '__main__': setup() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462640.9892776 librosa-1.0.0/tests/0000755000175100017510000000000015236640661013764 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2312272 librosa-1.0.0/tests/test_beat.py0000644000175100017510000001720115236640653016312 0ustar00runnerrunner#!/usr/bin/env python # CREATED:2013-03-11 18:14:30 by Brian McFee # unit tests for librosa.beat # Disable cache import os try: os.environ.pop("LIBROSA_CACHE_DIR") except KeyError: pass import pytest from contextlib import nullcontext as dnr import numpy as np import scipy.stats import librosa @pytest.fixture(scope="module", params=[22050, 44100]) def ysr(request): # Generate a pulse train at 120BPM y = np.zeros(5 * request.param) y[:: request.param // 2] = 1 return y, request.param @pytest.mark.parametrize("tempo", [60, 160]) @pytest.mark.parametrize("sr", [22050, 16000]) @pytest.mark.parametrize("hop_length", [512, 1024]) @pytest.mark.parametrize("ac_size", [4, 8]) @pytest.mark.parametrize("aggregate", [None, np.mean]) @pytest.mark.parametrize("prior", [None, scipy.stats.uniform(60, 240)]) def test_tempo(tempo, sr, hop_length, ac_size, aggregate, prior): y = np.zeros(20 * sr) delay = librosa.time_to_samples(60.0 / tempo, sr=sr).item() y[::delay] = 1 tempo_est = librosa.feature.tempo( y=y, sr=sr, hop_length=hop_length, ac_size=ac_size, aggregate=aggregate, prior=prior, ) # Being within 5% for the stable frames is close enough if aggregate is None: win_size = int(ac_size * sr // hop_length) assert np.all(np.abs(tempo_est[win_size:-win_size] - tempo) <= 0.05 * tempo) else: assert np.abs(tempo_est - tempo) <= 0.05 * tempo, (tempo, tempo_est) @pytest.mark.xfail(raises=librosa.ParameterError) def test_beat_no_input(): librosa.beat.beat_track(y=None, onset_envelope=None) def test_beat_no_onsets(): sr = 22050 hop_length = 512 duration = 30 onsets = np.zeros(duration * sr // hop_length) # Sparse case tempo, beats = librosa.beat.beat_track( onset_envelope=onsets, sr=sr, hop_length=hop_length, sparse=True ) assert np.allclose(tempo, 0) assert len(beats) == 0 # Dense case tempo, beats = librosa.beat.beat_track( onset_envelope=onsets, sr=sr, hop_length=hop_length, sparse=False ) assert np.allclose(tempo, 0) assert beats.shape == onsets.shape assert not np.any(beats) @pytest.mark.parametrize("start_bpm", [40, 60, 117, 235]) @pytest.mark.parametrize("aggregate", [None, np.mean]) @pytest.mark.parametrize("onsets", [np.zeros(30 * 22050 // 512)]) @pytest.mark.parametrize("sr", [22050]) @pytest.mark.parametrize("hop_length", [512]) def test_tempo_no_onsets(start_bpm, aggregate, onsets, sr, hop_length): tempo = librosa.feature.tempo( onset_envelope=onsets, sr=sr, hop_length=hop_length, start_bpm=start_bpm, aggregate=aggregate, ) # Depending on bin resolution, we might not be able to match exactly assert np.allclose(tempo, start_bpm, atol=1e0) @pytest.fixture(scope="module") def hop(): return 512 @pytest.fixture(scope="module") def oenv(ysr, hop): y, sr = ysr return librosa.onset.onset_strength(y=y, sr=sr, hop_length=hop) @pytest.mark.parametrize("with_audio", [False, True]) @pytest.mark.parametrize("trim", [False, True]) @pytest.mark.parametrize("start_bpm", [60, 120, 240]) @pytest.mark.parametrize("bpm", [None, 150, 360]) @pytest.mark.parametrize("tightness", [1e2, 1e4]) @pytest.mark.parametrize("prior", [None, scipy.stats.uniform(60, 240)]) def test_beat(ysr, hop, oenv, with_audio, start_bpm, bpm, trim, tightness, prior): y, sr = ysr if with_audio: _y = y _ons = None else: _y = None _ons = oenv tempo, beats = librosa.beat.beat_track( y=_y, sr=sr, onset_envelope=_ons, hop_length=hop, start_bpm=start_bpm, tightness=tightness, trim=trim, bpm=bpm, prior=prior, ) assert tempo >= 0 if len(beats) > 0: assert beats.min() >= 0 assert beats.max() <= len(oenv) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("tightness", [-1, -0.5, 0]) def test_beat_bad_tightness(ysr, tightness): y, sr = ysr librosa.beat.beat_track(y=y, sr=sr, tightness=tightness) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("bpm", [-1, -0.5, 0]) def test_beat_bad_bpm(ysr, bpm): y, sr = ysr librosa.beat.beat_track(y=y, sr=sr, bpm=bpm) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("start_bpm", [-1, -0.5, 0]) def test_beat_bad_start_bpm(ysr, start_bpm): y, sr = ysr librosa.beat.beat_track(y=y, sr=sr, start_bpm=start_bpm) @pytest.mark.parametrize("hop_length", [512, 1024]) @pytest.mark.parametrize( "units,ctx", [ ("frames", dnr()), ("time", dnr()), ("samples", dnr()), ("bad units", pytest.raises(librosa.ParameterError)), ], ) def test_beat_units(ysr, hop_length, units, ctx): y, sr = ysr with ctx: _, b2 = librosa.beat.beat_track(y=y, sr=sr, hop_length=hop_length, units=units) tempo, b1 = librosa.beat.beat_track(y=y, sr=sr, hop_length=hop_length) t1 = librosa.frames_to_time(b1, sr=sr, hop_length=hop_length) if units == "time": t2 = b2 elif units == "samples": t2 = librosa.samples_to_time(b2, sr=sr) elif units == "frames": t2 = librosa.frames_to_time(b2, sr=sr, hop_length=hop_length) else: assert False assert np.allclose(t1, t2) def test_beat_sparse(ysr): y, sr = ysr tempo, beats = librosa.beat.beat_track(y=y, sr=sr, sparse=True) _, beatsd = librosa.beat.beat_track(y=y, sr=sr, sparse=False) # Verify that frame indices correspond to detections assert np.all(beatsd[beats]) assert not np.any(~beatsd[beats]) @pytest.mark.parametrize("hop_length", [256, 512]) @pytest.mark.parametrize("win_length", [192, 384]) @pytest.mark.parametrize("use_onset", [False, True]) @pytest.mark.parametrize( "tempo_min,tempo_max,ctx", [ (30, 300, dnr()), (None, 240, dnr()), (60, None, dnr()), (120, 80, pytest.raises(librosa.ParameterError)), ], ) @pytest.mark.parametrize( "prior", [None, scipy.stats.lognorm(s=1, loc=np.log(120), scale=120)] ) @pytest.mark.filterwarnings( "ignore:n_fft=.*is too large" ) # our test signal is short, but this is fine here def test_plp(ysr, hop_length, win_length, tempo_min, tempo_max, use_onset, prior, ctx): y, sr = ysr oenv = librosa.onset.onset_strength(y=y, sr=sr, hop_length=hop_length) with ctx: if use_onset: pulse = librosa.beat.plp( y=y, sr=sr, onset_envelope=oenv, hop_length=hop_length, win_length=win_length, tempo_min=tempo_min, tempo_max=tempo_max, prior=prior, ) else: pulse = librosa.beat.plp( y=y, sr=sr, hop_length=hop_length, win_length=win_length, tempo_min=tempo_min, tempo_max=tempo_max, prior=prior, ) assert len(pulse) == len(oenv) assert np.all(pulse >= 0) assert np.all(pulse <= 1) def test_tempo_tgin(ysr): # Test that tempo estimation tempogram input matches without y, sr = ysr # Use a non-standard win length ac_size = 5 t1 = librosa.feature.tempo(y=y, sr=sr, ac_size=ac_size, aggregate=None) win_length = librosa.time_to_frames(ac_size, sr=sr).item() tg = librosa.feature.tempogram(y=y, sr=sr, win_length=win_length) t2 = librosa.feature.tempo(tg=tg, sr=sr, aggregate=None) assert np.allclose(t1, t2) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2312272 librosa-1.0.0/tests/test_cache.py0000644000175100017510000000245315236640653016445 0ustar00runnerrunner#!/usr/bin/env python # -*- encoding: utf-8 -*- # CREATED:2015-02-14 16:20:25 by Brian McFee # unit tests for librosa cache """Tests for librosa.cache""" import os import sys import tempfile import shutil import numpy as np import pytest import librosa._cache # Disable any initial cache settings for key in ["DIR", "MMAP", "COMPRESS", "VERBOSE", "LEVEL"]: try: os.environ.pop("LIBROSA_CACHE_{:s}".format(key)) except KeyError: pass @pytest.fixture def local_cache(): cache_dir = tempfile.mkdtemp() cache = librosa._cache.CacheManager(cache_dir, verbose=0, level=10) yield cache shutil.rmtree(cache_dir) def func(x): return np.arange(x) def test_cache_disabled(): # When there's no cache directory in the environment, # librosa.cache is a no-op. cache = librosa._cache.CacheManager(None, verbose=0, level=10) func_cache = cache(level=-10)(func) assert func == func_cache def test_cache_enabled(local_cache): local_cache.clear() func_cache = local_cache(level=-10)(func) # The cache should be active now, so func_cache should be a different object from func assert func_cache != func # issue three calls to func y = func(5) for i in range(3): assert np.allclose(func_cache(5), y) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2312272 librosa-1.0.0/tests/test_constantq.py0000644000175100017510000006567015236640653017426 0ustar00runnerrunner#!/usr/bin/env python """ CREATED:2015-03-01 by Eric Battenberg unit tests for librosa core.constantq """ from __future__ import division import warnings # Disable cache import os try: os.environ.pop("LIBROSA_CACHE_DIR") except KeyError: pass from typing import Optional import librosa import numpy as np import scipy.stats import pytest def __test_cqt_size( y, sr, hop_length, fmin, n_bins, bins_per_octave, tuning, filter_scale, norm, sparsity, res_type, ): cqt_output = np.abs( librosa.cqt( y, sr=sr, hop_length=hop_length, fmin=fmin, n_bins=n_bins, bins_per_octave=bins_per_octave, tuning=tuning, filter_scale=filter_scale, norm=norm, sparsity=sparsity, res_type=res_type, ) ) assert cqt_output.shape[0] == n_bins return cqt_output def make_signal(sr, duration, fmin: Optional[str] = "C1", fmax: Optional[str] = "C8"): """Generates a linear sine sweep""" if fmin is None: fmin_normfreq = 0.01 else: fmin_normfreq = librosa.note_to_hz(fmin) / sr if fmax is None: fmax_normfreq = 0.5 else: fmax_normfreq = librosa.note_to_hz(fmax) / sr return np.sin( np.cumsum( 2 * np.pi * np.logspace( np.log10(fmin_normfreq), np.log10(fmax_normfreq), num=int(duration * sr) ) ) ) @pytest.fixture(scope="module") def sr_cqt(): return 11025 @pytest.fixture(scope="module") def y_cqt(sr_cqt): return make_signal(sr_cqt, 2.0) @pytest.fixture(scope="module") def y_cqt_110(sr_cqt): return librosa.tone(110.0, sr=sr_cqt, duration=0.75) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("hop_length", [-1, 0]) @pytest.mark.parametrize("bpo", [12, 24]) def test_cqt_bad_hop(y_cqt, sr_cqt, hop_length, bpo): # incorrect hop lengths for a 6-octave analysis # num_octaves = 6, 2**(6-1) = 32 > 15 librosa.cqt( y=y_cqt, sr=sr_cqt, hop_length=hop_length, n_bins=bpo * 6, bins_per_octave=bpo ) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("bpo", [12, 24]) def test_cqt_exceed_passband(y_cqt, sr_cqt, bpo): # Filters going beyond nyquist: 500 Hz -> 4 octaves = 8000 > 11025/2 librosa.cqt(y=y_cqt, sr=sr_cqt, fmin=500, n_bins=4 * bpo, bins_per_octave=bpo) @pytest.mark.parametrize("fmin", [None, librosa.note_to_hz("C2")]) @pytest.mark.parametrize("n_bins", [1, 12, 24, 76]) @pytest.mark.parametrize("bins_per_octave", [12, 24]) @pytest.mark.parametrize("tuning", [None, 0, 0.25]) @pytest.mark.parametrize("filter_scale", [1]) @pytest.mark.parametrize("norm", [1]) @pytest.mark.parametrize("res_type", ["polyphase"]) @pytest.mark.parametrize("hop_length", [512, 2000]) @pytest.mark.parametrize("sparsity", [0.01]) @pytest.mark.filterwarnings("ignore:n_fft=.*is too large") # this is fine here @pytest.mark.filterwarnings( "ignore:Trying to estimate tuning" ) # we can ignore this too def test_cqt( y_cqt_110, sr_cqt, hop_length, fmin, n_bins, bins_per_octave, tuning, filter_scale, norm, res_type, sparsity, ): C = librosa.cqt( y=y_cqt_110, sr=sr_cqt, hop_length=hop_length, fmin=fmin, n_bins=n_bins, bins_per_octave=bins_per_octave, tuning=tuning, filter_scale=filter_scale, norm=norm, sparsity=sparsity, res_type=res_type, ) # type is complex assert np.iscomplexobj(C) # number of bins is correct assert C.shape[0] == n_bins if fmin is None: fmin = librosa.note_to_hz("C1") # check for peaks if 110 is within range if 110 <= fmin * 2 ** (n_bins / bins_per_octave): peaks = np.argmax(np.abs(C), axis=0) # This is our most common peak index in the CQT spectrum # we use the mode here over frames to sidestep transient effects # at the beginning and end of the CQT # common_peak = scipy.stats.mode(peaks, keepdims=True)[0][0] common_peak = np.argmax(np.bincount(peaks)) # Convert peak index to frequency peak_frequency = fmin * 2 ** (common_peak / bins_per_octave) # Check that it matches 110, which is an analysis frequency assert np.isclose(peak_frequency, 110) @pytest.mark.parametrize("hop_length", [256, 512]) def test_cqt_frame_rate(y_cqt_110, sr_cqt, hop_length): C = librosa.cqt(y=y_cqt_110, sr=sr_cqt, hop_length=hop_length, res_type="polyphase") # At sr=11025, hop of 256 gives 17 frames for default # analysis # hop of 512 gives 33 frames if hop_length == 256: assert C.shape[1] == 33 elif hop_length == 512: assert C.shape[1] == 17 else: # Unsupported test case assert False def test_cqt_odd_hop(y_cqt_110, sr_cqt): C = librosa.cqt(y=y_cqt_110, sr=sr_cqt, hop_length=1001, res_type="polyphase") def test_icqt_odd_hop(y_cqt_110, sr_cqt): C = librosa.cqt(y=y_cqt_110, sr=sr_cqt, hop_length=1001, res_type="polyphase") yi = librosa.icqt( C, sr=sr_cqt, hop_length=1001, res_type="polyphase", length=len(y_cqt_110) ) @pytest.mark.parametrize("fmin", [None, librosa.note_to_hz("C2")]) @pytest.mark.parametrize("n_bins", [1, 12, 24]) @pytest.mark.parametrize("gamma", [None, 0, 2.5]) @pytest.mark.parametrize("bins_per_octave", [12, 24]) @pytest.mark.parametrize("tuning", [0]) @pytest.mark.parametrize("filter_scale", [1]) @pytest.mark.parametrize("norm", [1]) @pytest.mark.parametrize("res_type", ["polyphase"]) @pytest.mark.parametrize("sparsity", [0.01]) @pytest.mark.parametrize("hop_length", [512]) @pytest.mark.filterwarnings("ignore:n_fft=.*is too large") def test_vqt( y_cqt_110, sr_cqt, hop_length, fmin, n_bins, gamma, bins_per_octave, tuning, filter_scale, norm, res_type, sparsity, ): C = librosa.vqt( y=y_cqt_110, sr=sr_cqt, hop_length=hop_length, fmin=fmin, n_bins=n_bins, gamma=gamma, bins_per_octave=bins_per_octave, tuning=tuning, filter_scale=filter_scale, norm=norm, sparsity=sparsity, res_type=res_type, ) # type is complex assert np.iscomplexobj(C) # number of bins is correct assert C.shape[0] == n_bins if fmin is None: fmin = librosa.note_to_hz("C1") # check for peaks if 110 is within range if 110 <= fmin * 2 ** (n_bins / bins_per_octave): peaks = np.argmax(np.abs(C), axis=0) # This is our most common peak index in the CQT spectrum # we use the mode here over frames to sidestep transient effects # at the beginning and end of the CQT # common_peak = scipy.stats.mode(peaks, keepdims=True)[0][0] common_peak = np.argmax(np.bincount(peaks)) # Convert peak index to frequency peak_frequency = fmin * 2 ** (common_peak / bins_per_octave) # Check that it matches 110, which is an analysis frequency assert np.isclose(peak_frequency, 110) @pytest.fixture(scope="module") def y_hybrid(): return make_signal(11025, 5.0, None) @pytest.mark.parametrize("sr", [11025]) @pytest.mark.parametrize("hop_length", [512, 2000]) @pytest.mark.parametrize("sparsity", [0.01]) @pytest.mark.parametrize("fmin", [None, librosa.note_to_hz("C2")]) @pytest.mark.parametrize("n_bins", [1, 12, 48, 76]) @pytest.mark.parametrize("bins_per_octave", [12, 24]) @pytest.mark.parametrize("tuning", [None, 0, 0.25]) @pytest.mark.parametrize("resolution", [1]) @pytest.mark.parametrize("norm", [1]) @pytest.mark.parametrize("res_type", ["polyphase"]) def test_hybrid_cqt( y_hybrid, sr, hop_length, fmin, n_bins, bins_per_octave, tuning, resolution, norm, sparsity, res_type, ): # This test verifies that hybrid and full cqt agree down to 1e-4 # on 99% of bins which are nonzero (> 1e-8) in either representation. C2 = librosa.hybrid_cqt( y_hybrid, sr=sr, hop_length=hop_length, fmin=fmin, n_bins=n_bins, bins_per_octave=bins_per_octave, tuning=tuning, filter_scale=resolution, norm=norm, sparsity=sparsity, res_type=res_type, ) C1 = np.abs( librosa.cqt( y_hybrid, sr=sr, hop_length=hop_length, fmin=fmin, n_bins=n_bins, bins_per_octave=bins_per_octave, tuning=tuning, filter_scale=resolution, norm=norm, sparsity=sparsity, res_type=res_type, ) ) assert C1.shape == C2.shape # Check for numerical comparability idx1 = C1 > 1e-4 * C1.max() idx2 = C2 > 1e-4 * C2.max() perc = 0.99 thresh = 1e-3 idx = idx1 | idx2 assert np.percentile(np.abs(C1[idx] - C2[idx]), perc) < thresh * max( C1.max(), C2.max() ) @pytest.mark.parametrize("note_min", [12, 18, 24, 30, 36]) @pytest.mark.parametrize("sr", [22050]) @pytest.mark.parametrize( "y", [np.sin(2 * np.pi * librosa.midi_to_hz(60) * np.arange(2 * 22050) / 22050.0)] ) def test_cqt_position(y, sr, note_min: int): C = np.abs(librosa.cqt(y, sr=sr, fmin=float(librosa.midi_to_hz(note_min)))) ** 2 # Average over time Cbar = np.median(C, axis=1) # Find the peak idx = int(np.argmax(Cbar)) assert idx == 60 - note_min # Make sure that the max outside the peak is sufficiently small Cscale = Cbar / Cbar[idx] Cscale[idx] = np.nan assert np.nanmax(Cscale) < 6e-1, Cscale Cscale[idx - 1 : idx + 2] = np.nan assert np.nanmax(Cscale) < 5e-2, Cscale @pytest.mark.xfail(raises=librosa.ParameterError) def test_cqt_fail_short_early(): # sampling rate is sufficiently above the top octave to trigger early downsampling y = np.zeros(16) librosa.cqt(y, sr=44100, n_bins=36) @pytest.fixture(scope="module", params=[11025, 16384, 22050, 32000, 44100]) def sr_impulse(request): return request.param @pytest.fixture(scope="module", params=range(1, 9)) def hop_impulse(request): return 64 * request.param @pytest.fixture(scope="module") def y_impulse(sr_impulse, hop_impulse): x = np.zeros(sr_impulse) center = int((len(x) / (2.0 * float(hop_impulse))) * hop_impulse) x[center] = 1 return x @pytest.mark.filterwarnings("ignore:n_fft=.*is too large") def test_cqt_impulse(y_impulse, sr_impulse, hop_impulse): # Test to resolve issue #348 # Updated in #417 to use integrated energy, rather than frame-wise max C = np.abs(librosa.cqt(y=y_impulse, sr=sr_impulse, hop_length=hop_impulse)) response = np.mean(C**2, axis=1) continuity = np.abs(np.diff(response)) # Test that integrated energy is approximately constant assert np.max(continuity) < 5e-4, continuity @pytest.mark.filterwarnings("ignore:n_fft=.*is too large") def test_hybrid_cqt_impulse(y_impulse, sr_impulse, hop_impulse): # Test to resolve issue #341 # Updated in #417 to use integrated energy instead of pointwise max hcqt = librosa.hybrid_cqt( y=y_impulse, sr=sr_impulse, hop_length=hop_impulse, tuning=0 ) response = np.mean(np.abs(hcqt) ** 2, axis=1) continuity = np.abs(np.diff(response)) assert np.max(continuity) < 5e-4, continuity @pytest.fixture(scope="module") def sr_white(): return 22050 @pytest.fixture(scope="module") def y_white(sr_white, rng_mod): return rng_mod.standard_normal(size=10 * sr_white) @pytest.mark.parametrize("scale", [False, True]) @pytest.mark.parametrize("fmin", list(librosa.note_to_hz(["C1", "C2"]))) @pytest.mark.parametrize("n_bins", [24, 36]) def test_cqt_white_noise(y_white, sr_white, fmin, n_bins, scale): C = np.abs( librosa.cqt(y=y_white, sr=sr_white, fmin=fmin, n_bins=n_bins, scale=scale) ) if not scale: freqs = librosa.cqt_frequencies(fmin=fmin, n_bins=n_bins) lengths, _ = librosa.filters.wavelet_lengths(sr=sr_white, freqs=freqs) C /= np.sqrt(lengths[:, np.newaxis]) # Only compare statistics across the time dimension # we want ~ constant mean and variance across frequencies assert np.allclose(np.mean(C, axis=1), 1.0, atol=2.5e-1), np.mean(C, axis=1) assert np.allclose(np.std(C, axis=1), 0.5, atol=5e-1), np.std(C, axis=1) @pytest.mark.parametrize("scale", [False, True]) @pytest.mark.parametrize("fmin", list(librosa.note_to_hz(["C1", "C2"]))) @pytest.mark.parametrize("n_bins", [72, 84]) def test_hybrid_cqt_white_noise(y_white, sr_white, fmin, n_bins, scale): C = librosa.hybrid_cqt( y=y_white, sr=sr_white, fmin=fmin, n_bins=n_bins, scale=scale ) if not scale: freqs = fmin * 2.0 ** (np.arange(n_bins) / 12) lengths, _ = librosa.filters.wavelet_lengths(freqs=freqs, sr=sr_white) C /= np.sqrt(lengths[:, np.newaxis]) assert np.allclose(np.mean(C, axis=1), 1.0, atol=2.5e-1), np.mean(C, axis=1) assert np.allclose(np.std(C, axis=1), 0.5, atol=5e-1), np.std(C, axis=1) @pytest.fixture(scope="module", params=[22050, 16000]) def sr_icqt(request): return request.param @pytest.fixture(scope="module") def y_icqt(sr_icqt): return make_signal(sr_icqt, 1.5, fmin="C2", fmax="C4") @pytest.mark.parametrize("over_sample", [1, 3]) @pytest.mark.parametrize("scale", [False, True]) @pytest.mark.parametrize("hop_length", [384, 512]) @pytest.mark.parametrize("res_type", ["soxr_lq", "polyphase"]) @pytest.mark.filterwarnings( "ignore:n_fft=.*is too large" ) # our test signal is short; this is fine def test_icqt(y_icqt, sr_icqt, scale, hop_length, over_sample, res_type): bins_per_octave = over_sample * 12 n_bins = 7 * bins_per_octave C = librosa.cqt( y_icqt, sr=sr_icqt, n_bins=n_bins, bins_per_octave=bins_per_octave, scale=scale, hop_length=hop_length, res_type=res_type, ) _len = len(y_icqt) yinv = librosa.icqt( C, sr=sr_icqt, scale=scale, hop_length=hop_length, bins_per_octave=bins_per_octave, length=_len, res_type=res_type, ) # Only test on the middle section assert len(y_icqt) == len(yinv) y_icqt = y_icqt[sr_icqt // 2 : -sr_icqt // 2] yinv = yinv[sr_icqt // 2 : -sr_icqt // 2] residual = np.abs(y_icqt - yinv) # We'll tolerate 6% RMSE # error is lower on more recent numpy/scipy builds resnorm = np.sqrt(np.mean(residual**2)) assert resnorm <= 0.06, resnorm @pytest.mark.parametrize("hop_length", [384, 512]) def test_icqt_nolength(y_icqt, sr_icqt, hop_length): bins_per_octave = 12 n_bins = 7 * bins_per_octave C = librosa.cqt( y_icqt, sr=sr_icqt, n_bins=n_bins, bins_per_octave=bins_per_octave, scale=True, hop_length=hop_length, ) yinv = librosa.icqt( C, sr=sr_icqt, scale=True, hop_length=hop_length, bins_per_octave=bins_per_octave, length=None, res_type="polyphase", ) yinv = librosa.util.fix_length(yinv, size=len(y_icqt)) y_icqt = y_icqt[sr_icqt // 2 : -sr_icqt // 2] yinv = yinv[sr_icqt // 2 : -sr_icqt // 2] residual = np.abs(y_icqt - yinv) resnorm = np.sqrt(np.mean(residual**2)) assert resnorm <= 0.06, resnorm def test_icqt_dtype(y_icqt, sr_icqt): C = librosa.cqt(y_icqt, sr=sr_icqt) y = librosa.icqt(C, sr=sr_icqt, dtype=np.float32) assert y.dtype == np.float32 y = librosa.icqt(C, sr=sr_icqt, dtype=np.float64) assert y.dtype == np.float64 @pytest.fixture(scope="module") def y_chirp(): sr = 22050 y = librosa.chirp(fmin=55, fmax=55 * 2**3, length=sr // 8, sr=sr) return y @pytest.mark.parametrize("hop_length", [512, 1024]) @pytest.mark.parametrize("window", ["hann", "hamming"]) @pytest.mark.parametrize("use_length", [False, True]) @pytest.mark.parametrize("over_sample", [1, 3]) @pytest.mark.parametrize("res_type", ["polyphase"]) @pytest.mark.parametrize("pad_mode", ["reflect"]) @pytest.mark.parametrize("scale", [False, True]) @pytest.mark.parametrize("momentum", [0.99]) @pytest.mark.parametrize("rng", [0]) @pytest.mark.parametrize("fmin", [40.0]) @pytest.mark.parametrize("dtype", [np.float32]) @pytest.mark.parametrize("init", [None]) @pytest.mark.filterwarnings("ignore:n_fft=.*is too large") def test_griffinlim_cqt( y_chirp, hop_length, window, use_length, over_sample, fmin, res_type, pad_mode, scale, momentum, init, rng, dtype, ): if use_length: length = len(y_chirp) else: length = None sr = 22050 bins_per_octave = 12 * over_sample n_bins = 6 * bins_per_octave C = librosa.cqt( y_chirp, sr=sr, hop_length=hop_length, window=window, fmin=fmin, bins_per_octave=bins_per_octave, n_bins=n_bins, scale=scale, pad_mode=pad_mode, res_type=res_type, ) Cmag = np.abs(C) y_rec = librosa.griffinlim_cqt( Cmag, hop_length=hop_length, window=window, sr=sr, fmin=fmin, bins_per_octave=bins_per_octave, scale=scale, pad_mode=pad_mode, n_iter=1, momentum=momentum, rng=rng, length=length, res_type=res_type, init=init, dtype=dtype, ) y_inv = librosa.icqt( Cmag, sr=sr, fmin=fmin, hop_length=hop_length, window=window, bins_per_octave=bins_per_octave, scale=scale, length=length, res_type=res_type, ) # First check for length if use_length: assert len(y_rec) == length # Check that the data is okay assert np.all(np.isfinite(y_rec)) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) @pytest.mark.filterwarnings("ignore:n_fft=.*is too large") def test_griffinlim_cqt_dtype(y_chirp, dtype): C = librosa.cqt(y_chirp, sr=22050, res_type="polyphase") y = librosa.griffinlim_cqt( np.abs(C), sr=22050, dtype=dtype, n_iter=2, res_type="polyphase" ) assert y.dtype == dtype @pytest.mark.parametrize("momentum", [0, 0.95]) @pytest.mark.filterwarnings("ignore:n_fft=.*is too large") def test_griffinlim_cqt_momentum(y_chirp, momentum): C = librosa.cqt(y=y_chirp, sr=22050, res_type="polyphase") y_rec = librosa.griffinlim_cqt( np.abs(C), sr=22050, n_iter=2, momentum=momentum, res_type="polyphase" ) assert np.all(np.isfinite(y_rec)) @pytest.mark.parametrize("rng", [None, 0, np.random.RandomState()]) @pytest.mark.filterwarnings("ignore:n_fft=.*is too large") def test_griffinlim_cqt_rng(y_chirp, rng): C = librosa.cqt(y=y_chirp, sr=22050, res_type="polyphase") y_rec = librosa.griffinlim_cqt( np.abs(C), sr=22050, n_iter=2, rng=rng, res_type="polyphase" ) y_rec2 = librosa.griffinlim_cqt( np.abs(C), sr=22050, n_iter=2, rng=rng, res_type="polyphase" ) if not isinstance(rng, np.random.RandomState) and rng is not None: assert np.allclose(y_rec, y_rec2) @pytest.mark.parametrize("init", [None, "random"]) @pytest.mark.filterwarnings("ignore:n_fft=.*is too large") def test_griffinlim_cqt_init(y_chirp, init): C = librosa.cqt(y=y_chirp, sr=22050, res_type="polyphase") y_rec = librosa.griffinlim_cqt( np.abs(C), sr=22050, n_iter=2, init=init, res_type="polyphase" ) assert np.all(np.isfinite(y_rec)) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.filterwarnings("ignore:n_fft=.*is too large") def test_griffinlim_cqt_badinit(): x = np.zeros((33, 3)) librosa.griffinlim_cqt(x, init="garbage") @pytest.mark.xfail(raises=TypeError) @pytest.mark.filterwarnings("ignore:n_fft=.*is too large") def test_griffinlim_cqt_badrng(): x = np.zeros((33, 3)) librosa.griffinlim_cqt(x, rng="garbage") # type: ignore @pytest.mark.xfail(raises=librosa.ParameterError) def test_griffinlim_cqt_bad_momentum(): x = np.zeros((33, 3)) librosa.griffinlim_cqt(x, momentum=-1) def test_griffinlim_cqt_momentum_warn(): x = np.zeros((33, 3)) with pytest.warns(UserWarning): librosa.griffinlim_cqt(x, momentum=2) def test_griffinlim_cqt_deprecated_randomstate(y_chirp): # Test that the deprecated random_state argument raises a warning C = librosa.cqt(y=y_chirp, sr=22050, res_type="polyphase") with pytest.warns(FutureWarning, match="renamed to 'rng'"): y1 = librosa.griffinlim_cqt( np.abs(C), sr=22050, n_iter=2, random_state=5, res_type="polyphase", ) # And verify that it produces the same output as the new rng argument y2 = librosa.griffinlim_cqt( np.abs(C), sr=22050, n_iter=2, rng=5, res_type="polyphase", ) assert np.allclose(y1, y2) @pytest.mark.xfail(raises=librosa.ParameterError) def test_griffinlim_cqt_oldrng(): x = np.zeros((33, 3)) # We can't have both parameters set librosa.griffinlim_cqt(x, rng=0, random_state=0) @pytest.mark.parametrize("dtype", [np.complex64, np.complex128]) def test_cqt_precision(y_cqt, sr_cqt, dtype): C = librosa.cqt(y=y_cqt, sr=sr_cqt, dtype=dtype) assert np.dtype(C.dtype) == np.dtype(dtype) @pytest.mark.parametrize("n_bins_missing", range(-11, 11)) def test_cqt_partial_octave(y_cqt, sr_cqt, n_bins_missing): # Test what happens when n_bins is +- 1 bin from complete octaves librosa.cqt(y=y_cqt, sr=sr_cqt, n_bins=72 - n_bins_missing, bins_per_octave=12) def test_vqt_provided_intervals(y_cqt, sr_cqt): # Generate a 20-ET vqt V1 = librosa.vqt( y=y_cqt, sr=sr_cqt, bins_per_octave=20, n_bins=60, intervals="equal" ) # Generate the same thing with a pre-set list of intervals intervals = 2.0 ** (np.arange(20) / 20.0) V2 = librosa.vqt(y=y_cqt, sr=sr_cqt, n_bins=60, intervals=intervals) assert np.allclose(V1, V2) @pytest.mark.parametrize("bins_per_octave", [12, 24, 60]) def test_cqt_nbins_none(y_cqt, sr_cqt, bins_per_octave): # Test that n_bins=None works C = librosa.cqt(y=y_cqt, sr=sr_cqt, n_bins=None, bins_per_octave=bins_per_octave) # We'll compare to explicitly setting n_bins to match C2 = librosa.cqt(y=y_cqt, sr=sr_cqt, n_bins=C.shape[-2], bins_per_octave=bins_per_octave) assert np.allclose(C, C2) # Now verify that going one step further would raise an error with pytest.raises(librosa.ParameterError): librosa.cqt(y=y_cqt, sr=sr_cqt, n_bins=C.shape[-2] + 1, bins_per_octave=bins_per_octave) @pytest.mark.parametrize("bins_per_octave", [12, 24, 60]) @pytest.mark.parametrize("intervals", ["equal", "ji5", "ji7"]) @pytest.mark.parametrize("gamma", [None, 0]) @pytest.mark.parametrize("fmin", [32, 64, 128]) def test_vqt_nbins_none(y_cqt, sr_cqt, bins_per_octave, intervals, gamma, fmin): # Test that n_bins=None works for VQT V = librosa.vqt( y=y_cqt, sr=sr_cqt, fmin=fmin, n_bins=None, bins_per_octave=bins_per_octave, intervals=intervals, gamma=gamma, ) # We'll compare to explicitly setting n_bins to match V2 = librosa.vqt( y=y_cqt, sr=sr_cqt, fmin=fmin, n_bins=V.shape[-2], bins_per_octave=bins_per_octave, intervals=intervals, gamma=gamma, ) assert np.allclose(V, V2) # Now verify that going one step further would raise an error with pytest.raises(librosa.ParameterError): librosa.vqt( y=y_cqt, sr=sr_cqt, fmin=fmin, n_bins=V.shape[-2] + 1, bins_per_octave=bins_per_octave, intervals=intervals, gamma=gamma, ) def test_vqt_fmin_high(y_cqt): with pytest.raises(librosa.ParameterError): librosa.vqt(y=y_cqt, sr=2000, fmin=999, n_bins=2, bins_per_octave=1) def test_vqt_nbins_none_small(y_cqt): with pytest.raises(librosa.ParameterError): librosa.vqt(y=y_cqt, sr=2000, fmin=999, n_bins=None, bins_per_octave=1) def test_vqt_fmin_over(y_cqt): with pytest.raises(librosa.ParameterError): librosa.vqt(y=y_cqt, sr=2000, fmin=1500, n_bins=12) @pytest.mark.parametrize("bins_per_octave", [12, 24, 60]) @pytest.mark.parametrize("fmin", [32, 64, 128]) def test_hybrid_cqt_nbins_none(y_cqt, sr_cqt, bins_per_octave, fmin): # Test that n_bins=None works for hybrid CQT V = librosa.hybrid_cqt( y=y_cqt, sr=sr_cqt, fmin=fmin, n_bins=None, bins_per_octave=bins_per_octave, ) # We'll compare to explicitly setting n_bins to match V2 = librosa.hybrid_cqt( y=y_cqt, sr=sr_cqt, fmin=fmin, n_bins=V.shape[-2], bins_per_octave=bins_per_octave, ) assert np.allclose(V, V2) # Now verify that going one step further would raise an error with pytest.raises(librosa.ParameterError): librosa.hybrid_cqt( y=y_cqt, sr=sr_cqt, fmin=fmin, n_bins=V.shape[-2] + 1, bins_per_octave=bins_per_octave, ) def test_hybrid_cqt_fmin_high(y_cqt): with pytest.raises(librosa.ParameterError): librosa.hybrid_cqt(y=y_cqt, sr=2000, fmin=999, n_bins=2, bins_per_octave=1) def test_hybrid_cqt_nbins_none_small(y_cqt): with pytest.raises(librosa.ParameterError): librosa.hybrid_cqt(y=y_cqt, sr=2000, fmin=999, n_bins=None, bins_per_octave=1) def test_hybrid_cqt_fmin_over(y_cqt): with pytest.raises(librosa.ParameterError): librosa.hybrid_cqt(y=y_cqt, sr=2000, fmin=1500, n_bins=12) @pytest.mark.parametrize("bins_per_octave", [12, 24, 60]) @pytest.mark.parametrize("fmin", [32, 64, 128]) def test_pseudo_cqt_nbins_none(y_cqt, sr_cqt, bins_per_octave, fmin): # Test that n_bins=None works for pseudo CQT V = librosa.pseudo_cqt( y=y_cqt, sr=sr_cqt, fmin=fmin, n_bins=None, bins_per_octave=bins_per_octave, ) # We'll compare to explicitly setting n_bins to match V2 = librosa.pseudo_cqt( y=y_cqt, sr=sr_cqt, fmin=fmin, n_bins=V.shape[-2], bins_per_octave=bins_per_octave, ) assert np.allclose(V, V2) # Now verify that going one step further would raise an error with pytest.raises(librosa.ParameterError): librosa.pseudo_cqt( y=y_cqt, sr=sr_cqt, fmin=fmin, n_bins=V.shape[-2] + 1, bins_per_octave=bins_per_octave, ) def test_pseudo_cqt_fmin_high(y_cqt): with pytest.raises(librosa.ParameterError): librosa.pseudo_cqt(y=y_cqt, sr=2000, fmin=999, n_bins=2, bins_per_octave=1) def test_pseudo_cqt_nbins_none_small(y_cqt): with pytest.raises(librosa.ParameterError): librosa.pseudo_cqt(y=y_cqt, sr=2000, fmin=999, n_bins=None, bins_per_octave=1) def test_pseudo_cqt_fmin_over(y_cqt): with pytest.raises(librosa.ParameterError): librosa.pseudo_cqt(y=y_cqt, sr=2000, fmin=1500, n_bins=12) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1786462635.232227 librosa-1.0.0/tests/test_convert.py0000644000175100017510000005502615236640653017066 0ustar00runnerrunner#!/usr/bin/env python # -*- encoding: utf-8 -*- # CREATED:2015-02-14 19:13:49 by Brian McFee """Unit tests for time and frequency conversion""" import os import sys try: os.environ.pop("LIBROSA_CACHE_DIR") except KeyError: pass import warnings import librosa import numpy as np import pytest @pytest.mark.parametrize( "frames", [100, np.arange(10.0), np.ones((3, 3))], ids=["0d", "1d", "2d"] ) @pytest.mark.parametrize("hop_length", [512, 1024]) @pytest.mark.parametrize("n_fft", [None, 1024]) def test_frames_to_samples(frames, hop_length, n_fft): samples = librosa.frames_to_samples(frames, hop_length=hop_length, n_fft=n_fft) frames = np.asanyarray(frames) assert frames.shape == samples.shape assert frames.ndim == samples.ndim if n_fft is None: assert np.allclose(samples, frames * hop_length) else: assert np.allclose((samples - n_fft // 2) // hop_length, frames) @pytest.mark.parametrize( "samples", [1024 * 100, 1024 * np.arange(10.0), 1024 * np.ones((3, 3))], ids=["0d", "1d", "2d"], ) @pytest.mark.parametrize("hop_length", [512, 1024]) @pytest.mark.parametrize("n_fft", [None, 1024]) def test_samples_to_frames(samples, hop_length, n_fft): frames = librosa.samples_to_frames(samples, hop_length=hop_length, n_fft=n_fft) samples = np.asanyarray(samples) assert frames.shape == samples.shape assert frames.ndim == samples.ndim if n_fft is None: assert np.allclose(samples, frames * hop_length) else: assert np.allclose((samples - n_fft // 2) // hop_length, frames) @pytest.mark.parametrize("sr", [22050, 44100]) @pytest.mark.parametrize("hop_length", [256, 512]) @pytest.mark.parametrize("n_fft", [None, 2048]) def test_frames_to_time(sr, hop_length, n_fft): # Generate frames at times 0s, 1s, 2s frames = np.arange(3) * sr // hop_length if n_fft: frames -= n_fft // (2 * hop_length) times = librosa.frames_to_time(frames, sr=sr, hop_length=hop_length, n_fft=n_fft) # we need to be within one frame assert np.all(np.abs(times - np.asarray([0, 1, 2])) * sr < hop_length) @pytest.mark.parametrize("sr", [22050, 44100]) def test_time_to_samples(sr): assert np.allclose(librosa.time_to_samples([0, 1, 2], sr=sr), [0, sr, 2 * sr]) @pytest.mark.parametrize("sr", [22050, 44100]) def test_samples_to_time(sr): assert np.allclose(librosa.samples_to_time([0, sr, 2 * sr], sr=sr), [0, 1, 2]) @pytest.mark.parametrize("sr", [22050, 44100]) @pytest.mark.parametrize("hop_length", [256, 512]) @pytest.mark.parametrize("n_fft", [None, 2048]) def test_time_to_frames(sr, hop_length, n_fft): # Generate frames at times 0s, 1s, 2s times = np.arange(3) frames = librosa.time_to_frames(times, sr=sr, hop_length=hop_length, n_fft=n_fft) if n_fft: frames -= n_fft // (2 * hop_length) # we need to be within one frame assert np.all(np.abs(times - np.asarray([0, 1, 2])) * sr < hop_length) @pytest.mark.parametrize("tuning", [0.0, -0.2, 0.1]) @pytest.mark.parametrize("bins_per_octave", [12, 24, 36]) def test_octs_to_hz(tuning, bins_per_octave): freq = np.asarray([55, 110, 220, 440]) * (2.0 ** (tuning / bins_per_octave)) freq_out = librosa.octs_to_hz( [1, 2, 3, 4], tuning=tuning, bins_per_octave=bins_per_octave ) assert np.allclose(freq, freq_out) @pytest.mark.parametrize("tuning", [0.0, -0.2, 0.1]) @pytest.mark.parametrize("bins_per_octave", [12, 24, 36]) def test_hz_to_octs(tuning, bins_per_octave): freq = np.asarray([55, 110, 220, 440]) * (2.0 ** (tuning / bins_per_octave)) octs = [1, 2, 3, 4] oct_out = librosa.hz_to_octs(freq, tuning=tuning, bins_per_octave=bins_per_octave) assert np.allclose(octs, oct_out) @pytest.mark.parametrize( "A4,bins_per_octave,tuning", [ (440.0, 12, 0.0), ([440.0, 444.0], 24, [0.0, 0.31335]), ([432.0], 12, [-0.317667]), (432.0, 36, -0.953), ], ) def test_A4_to_tuning(A4, bins_per_octave, tuning): tuning_out = librosa.A4_to_tuning(A4=A4, bins_per_octave=bins_per_octave) assert np.allclose(np.asarray(tuning), tuning_out) @pytest.mark.parametrize( "tuning,bins_per_octave,A4", [ (0.0, 12, 440.0), ([-0.2], 24, [437.466]), ([0.1, 0.9], 36, [440.848, 447.691]), (0.0, 24, 440.0), ], ) def test_tuning_to_A4(tuning, bins_per_octave, A4): A4_out = librosa.tuning_to_A4(tuning=tuning, bins_per_octave=bins_per_octave) assert np.allclose(np.asarray(A4), A4_out) @pytest.mark.parametrize( "tuning,octave", [ (None, None), (None, 1), (None, 2), (None, 3), (-25, 1), (-25, 2), (-25, 3), (0, 1), (0, 2), (0, 3), ], ) @pytest.mark.parametrize("accidental", ["", "#", "b", "!"]) @pytest.mark.parametrize("round_midi", [False, True]) def test_note_to_midi(tuning, accidental, octave, round_midi: bool): note = "C{:s}".format(accidental) if octave is not None: note = "{:s}{:d}".format(note, octave) else: octave = 0 if tuning is not None: note = "{:s}{:+d}".format(note, tuning) else: tuning = 0 midi_true = 12 * (octave + 1) + tuning * 0.01 if accidental == "#": midi_true += 1 elif accidental in list("b!"): midi_true -= 1 midi = librosa.note_to_midi(note, round_midi=round_midi) if round_midi: midi_true = np.round(midi_true) assert midi == midi_true midi = librosa.note_to_midi([note], round_midi=round_midi) assert midi[0] == midi_true @pytest.mark.xfail(raises=librosa.ParameterError) def test_note_to_midi_badnote(): librosa.note_to_midi("does not pass") @pytest.mark.parametrize( "tuning,octave", [ (None, None), (None, 1), (None, 2), (None, 3), (-25, 1), (-25, 2), (-25, 3), (0, 1), (0, 2), (0, 3), ], ) @pytest.mark.parametrize("accidental", ["", "#", "b", "!"]) @pytest.mark.parametrize("round_midi", [False, True]) def test_note_to_hz(tuning, octave, accidental, round_midi: bool): note = "A{:s}".format(accidental) if octave is not None: note = "{:s}{:d}".format(note, octave) else: octave = 0 if tuning is not None: note = "{:s}{:+d}".format(note, tuning) else: tuning = 0 if round_midi: tuning = np.around(tuning, -2) hz_true = 440.0 * (2.0 ** (tuning * 0.01 / 12)) * (2.0 ** (octave - 4)) if accidental == "#": hz_true *= 2.0 ** (1.0 / 12) elif accidental in list("b!"): hz_true /= 2.0 ** (1.0 / 12) hz = librosa.note_to_hz(note, round_midi=round_midi) assert np.allclose(hz, hz_true) hz = librosa.note_to_hz([note], round_midi=round_midi) assert np.allclose(hz[0], hz_true) @pytest.mark.xfail(raises=librosa.ParameterError) def test_note_to_hz_badnote(): librosa.note_to_hz("does not pass") def test_note_to_hz_default(): hz = librosa.note_to_hz("C2-30", round_midi=False) assert np.isclose(hz, librosa.note_to_hz("C2-30")) @pytest.mark.parametrize( "midi_num,note,octave,cents", [ (24.25, "C", False, False), (24.25, "C1", True, False), (24.25, "C1+25", True, True), ([24.25], ["C"], False, False), ], ) def test_midi_to_note(midi_num, note, octave, cents): note_out = librosa.midi_to_note(midi_num, octave=octave, cents=cents) assert note_out == note @pytest.mark.xfail(raises=librosa.ParameterError) def test_midi_to_note_cents_nooctave(): librosa.midi_to_note(24.25, octave=False, cents=True) def test_midi_to_hz(): assert np.allclose(librosa.midi_to_hz([33, 45, 57, 69]), [55, 110, 220, 440]) def test_hz_to_midi(): assert np.allclose(librosa.hz_to_midi(55), 33) assert np.allclose(librosa.hz_to_midi([55, 110, 220, 440]), [33, 45, 57, 69]) @pytest.mark.parametrize( "hz,note,octave,cents", [ (440, "A", False, False), (440, "A4", True, False), (440, "A4+0", True, True), ([440, 880], ["A4+0", "A5+0"], True, True), ], ) def test_hz_to_note(hz, note, octave, cents): note_out = librosa.hz_to_note(hz, octave=octave, cents=cents) if np.isscalar(hz): assert note_out == note else: assert np.all(note_out == note) @pytest.mark.xfail(raises=librosa.ParameterError) def test_hz_to_note_cents_nooctave(): librosa.hz_to_note(440, octave=False, cents=True) @pytest.mark.parametrize("sr", [8000, 22050]) @pytest.mark.parametrize("n_fft", [1024, 2048]) def test_fft_frequencies(sr, n_fft): freqs = librosa.fft_frequencies(sr=sr, n_fft=n_fft) # DC assert freqs[0] == 0 # Nyquist, positive here for more convenient display purposes assert freqs[-1] == sr / 2.0 # Ensure that the frequencies increase linearly dels = np.diff(freqs) assert np.allclose(dels, dels[0]) @pytest.mark.parametrize("n_bins", [12, 24, 36]) @pytest.mark.parametrize("fmin", [440.0]) @pytest.mark.parametrize("bins_per_octave", [12, 24, 36]) @pytest.mark.parametrize("tuning", [-0.25, 0.0, 0.25]) def test_cqt_frequencies(n_bins, fmin, bins_per_octave, tuning): freqs = librosa.cqt_frequencies( n_bins, fmin=fmin, bins_per_octave=bins_per_octave, tuning=tuning ) # Make sure we get the right number of bins assert len(freqs) == n_bins # And that the first bin matches fmin by tuning assert np.allclose(freqs[0], fmin * 2.0 ** (float(tuning) / bins_per_octave)) # And that we have constant Q Q = np.diff(np.log2(freqs)) assert np.allclose(Q, 1.0 / bins_per_octave) @pytest.mark.parametrize("n_bins", [1, 16, 128]) @pytest.mark.parametrize("hop_length", [256, 512]) @pytest.mark.parametrize("sr", [11025, 22050]) def test_tempo_frequencies(n_bins, hop_length, sr): freqs = librosa.tempo_frequencies(n_bins, hop_length=hop_length, sr=sr) # Verify the length assert len(freqs) == n_bins # 0-bin should be infinite assert not np.isfinite(freqs[0]) # remaining bins should be spaced by 1/hop_length if n_bins > 1: invdiff = (freqs[1:] ** -1) * (60.0 * sr) assert np.allclose(invdiff[0], hop_length) assert np.allclose(np.diff(invdiff), np.asarray(hop_length)), np.diff(invdiff) @pytest.mark.parametrize("sr", [8000, 22050]) @pytest.mark.parametrize("hop_length", [256, 512]) @pytest.mark.parametrize("win_length", [192, 384]) def test_fourier_tempo_frequencies(sr, hop_length, win_length): freqs = librosa.fourier_tempo_frequencies( sr=sr, hop_length=hop_length, win_length=win_length ) # DC assert freqs[0] == 0 # Nyquist, positive here for more convenient display purposes assert freqs[-1] == sr * 60 / 2.0 / hop_length # Ensure that the frequencies increase linearly dels = np.diff(freqs) assert np.allclose(dels, dels[0]) @pytest.mark.parametrize("min_db", [None, -40, -80]) def test_A_weighting(min_db): # Check that 1KHz is around 0dB a_khz = librosa.A_weighting(1000.0, min_db=min_db) assert np.allclose(a_khz, 0, atol=1e-3) a_range = librosa.A_weighting(np.linspace(2e1, 2e4), min_db=min_db) # Check that the db cap works if min_db is not None: assert not np.any(a_range < min_db) @pytest.mark.parametrize("min_db", [None, -40, -80]) def test_B_weighting(min_db): # Check that 1KHz is around 0dB b_khz = librosa.B_weighting(1000.0, min_db=min_db) assert np.allclose(b_khz, 0, atol=1e-3) b_range = librosa.B_weighting(np.linspace(2e1, 2e4), min_db=min_db) # Check that the db cap works if min_db is not None: assert not np.any(b_range < min_db) @pytest.mark.parametrize("min_db", [None, -40, -80]) def test_C_weighting(min_db): # Check that 1KHz is around 0dB c_khz = librosa.C_weighting(1000.0, min_db=min_db) assert np.allclose(c_khz, 0, atol=1e-3) c_range = librosa.B_weighting(np.linspace(2e1, 2e4), min_db=min_db) # Check that the db cap works if min_db is not None: assert not np.any(c_range < min_db) @pytest.mark.parametrize("min_db", [None, -40, -80]) def test_D_weighting(min_db): # Check that 1KHz is around 0dB d_khz = librosa.D_weighting(1000.0, min_db=min_db) assert np.allclose(d_khz, 0, atol=1e-3) d_range = librosa.D_weighting(np.linspace(2e1, 2e4), min_db=min_db) # Check that the db cap works if min_db is not None: assert not np.any(d_range < min_db) @pytest.mark.parametrize("min_db", [None, -40, -80]) def test_Z_weighting(min_db): # Check that 1KHz is around 0dB d_khz = librosa.Z_weighting(np.linspace(2e1, 2e4), min_db=min_db) assert np.allclose(d_khz, 0, atol=1e-3) @pytest.mark.parametrize("kind", ["A", "B", "C", "D", "Z", None]) def test_frequency_weighting(kind): freq = np.linspace(2e1, 2e4) assert np.allclose( librosa.frequency_weighting(freq, kind=kind), librosa.core.convert.WEIGHTING_FUNCTIONS[kind](freq), 0, atol=1e-3, ) @pytest.mark.parametrize("kinds", ["AZC", ["A", "Z", "C"]]) def test_multi_frequency_weighting(kinds): freq = np.linspace(2e1, 2e4) assert np.allclose( librosa.multi_frequency_weighting(freq, kinds=kinds), np.stack( [ librosa.A_weighting(freq), librosa.Z_weighting(freq), librosa.C_weighting(freq), ] ), 0, atol=1e-3, ) def test_frequency_weighting_underflow_warning(): # Ensure that four warnings are issued for the following call with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") librosa.multi_frequency_weighting( np.arange(3), kinds="ABCD", min_db=None ) assert len(w) == 4 # Ensure that all warnings are about non-finite values for warn in w: assert issubclass(warn.category, UserWarning) assert "non-finite" in str(warn.message) # Now ensure that no warnings are issued when min_db is set with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") librosa.multi_frequency_weighting( np.arange(3), kinds="ABCD", min_db=-100 ) assert len(w) == 0, "No warnings should be issued with min_db set" def test_samples_like(): X = np.ones((3, 4, 5)) hop_length = 512 for axis in (0, 1, 2, -1): samples = librosa.samples_like(X, hop_length=hop_length, axis=axis) expected_samples = np.arange(X.shape[axis]) * hop_length assert np.allclose(samples, expected_samples) def test_samples_like_scalar(): X = 7 hop_length = 512 samples = librosa.samples_like(X, hop_length=hop_length) expected_samples = np.arange(7) * hop_length assert np.allclose(samples, expected_samples) def test_times_like(): X = np.ones((3, 4, 5)) sr = 22050 hop_length = 512 for axis in (0, 1, 2, -1): times = librosa.times_like(X, sr=sr, hop_length=hop_length, axis=axis) expected_times = np.arange(X.shape[axis]) * hop_length / float(sr) assert np.allclose(times, expected_times) def test_times_like_scalar(): X = 7 sr = 22050 hop_length = 512 times = librosa.times_like(X, sr=sr, hop_length=hop_length) expected_times = np.arange(7) * hop_length / float(sr) assert np.allclose(times, expected_times) @pytest.mark.parametrize("blocks", [0, 1, [10, 20]]) @pytest.mark.parametrize("block_length", [1, 4, 8]) def test_blocks_to_frames(blocks, block_length): frames = librosa.blocks_to_frames(blocks, block_length=block_length) # Check shape assert frames.ndim == np.asarray(blocks).ndim assert frames.size == np.asarray(blocks).size # Check values assert np.allclose(frames, block_length * np.asanyarray(blocks)) # Check dtype assert np.issubdtype(frames.dtype, int) @pytest.mark.parametrize("blocks", [0, 1, [10, 20]]) @pytest.mark.parametrize("block_length", [1, 4, 8]) @pytest.mark.parametrize("hop_length", [1, 512]) def test_blocks_to_samples(blocks, block_length, hop_length): samples = librosa.blocks_to_samples( blocks, block_length=block_length, hop_length=hop_length ) # Check shape assert samples.ndim == np.asarray(blocks).ndim assert samples.size == np.asarray(blocks).size # Check values assert np.allclose(samples, np.asanyarray(blocks) * hop_length * block_length) # Check dtype assert np.issubdtype(samples.dtype, int) @pytest.mark.parametrize("blocks", [0, 1, [10, 20]]) @pytest.mark.parametrize("block_length", [1, 4, 8]) @pytest.mark.parametrize("hop_length", [1, 512]) @pytest.mark.parametrize("sr", [22050, 44100]) def test_blocks_to_time(blocks, block_length, hop_length, sr): times = librosa.blocks_to_time( blocks, block_length=block_length, hop_length=hop_length, sr=sr ) # Check shape assert times.ndim == np.asarray(blocks).ndim assert times.size == np.asarray(blocks).size # Check values assert np.allclose( times, np.asanyarray(blocks) * hop_length * block_length / float(sr) ) # Check dtype assert np.issubdtype(times.dtype, float) @pytest.mark.parametrize("abbr", [False, True]) @pytest.mark.parametrize("octave", [False, True]) @pytest.mark.parametrize("unicode", [False, True]) @pytest.mark.parametrize("midi", [list(range(36))]) @pytest.mark.parametrize("Sa", [12]) def test_midi_to_svara_h(midi, Sa, abbr, octave, unicode): svara = librosa.midi_to_svara_h( midi, Sa=Sa, abbr=abbr, octave=octave, unicode=unicode ) svara = np.asarray(svara) assert len(svara) == len(midi) if abbr: assert svara[Sa] == "S" else: assert svara[Sa] == "Sa" if not unicode: for s in svara: assert s.isascii() if not abbr: for s in svara: assert 0 < len(s) < 5 else: for s in svara: assert 0 < len(s) < 3 # Octave decorations should separate out per octave if octave: assert not np.all(svara[:12] == svara[12:24]) assert not np.all(svara[12:24] == svara[24:]) else: assert np.all(svara[:12] == svara[12:24]) assert np.all(svara[:12] == svara[24:]) @pytest.mark.parametrize( "f,Sa,abbr,octave,unicode,result", [ (440, 440, False, False, True, "Sa"), (880, 440, False, False, True, "Sa"), (880, 440, True, False, True, "S"), (880, 440, True, True, False, "S'"), (880, 440, True, True, True, "Ṡ"), (880, 440, False, True, True, "Ṡa"), (220, 440, False, True, True, "Ṣa"), (660, 440, True, True, True, "P"), ], ) def test_hz_to_svara_h(f, Sa, abbr, octave, unicode, result): s = librosa.hz_to_svara_h(f, Sa=Sa, abbr=abbr, octave=octave, unicode=unicode) assert s == result @pytest.mark.parametrize( "note,Sa,abbr,octave,unicode,result", [ ("A4", "A4", False, False, True, "Sa"), ("A5", "A4", False, False, True, "Sa"), ("A5", "A4", True, False, True, "S"), ("A5", "A4", True, True, False, "S'"), ("A5", "A4", True, True, True, "Ṡ"), ("A5", "A4", False, True, True, "Ṡa"), ("A3", "A4", False, True, True, "Ṣa"), ("E5", "A4", True, True, True, "P"), ], ) def test_note_to_svara_h(note, Sa, abbr, octave, unicode, result): s = librosa.note_to_svara_h(note, Sa=Sa, abbr=abbr, octave=octave, unicode=unicode) assert s == result @pytest.mark.parametrize("abbr", [False, True]) @pytest.mark.parametrize("octave", [False, True]) @pytest.mark.parametrize("unicode", [False, True]) @pytest.mark.parametrize("midi", [list(range(36))]) @pytest.mark.parametrize("Sa", [12]) @pytest.mark.parametrize("mela", range(1, 72, 7)) def test_midi_to_svara_c(midi, Sa, mela, abbr, octave, unicode): svara = librosa.midi_to_svara_c( midi, Sa=Sa, mela=mela, abbr=abbr, octave=octave, unicode=unicode ) svara = np.asarray(svara) assert len(svara) == len(midi) if abbr: assert svara[Sa] == "S" else: assert svara[Sa] == "Sa" if not unicode: for s in svara: assert s.isascii() if not abbr: for s in svara: # Lengths for unicode can get lengthy, eg # Dha2' assert 0 < len(s) < 6 else: for s in svara: assert 0 < len(s) < 4 # Octave decorations should separate out per octave if octave: assert not np.all(svara[:12] == svara[12:24]) assert not np.all(svara[12:24] == svara[24:]) else: assert np.all(svara[:12] == svara[12:24]) assert np.all(svara[:12] == svara[24:]) @pytest.mark.parametrize( "freq,Sa,mela,abbr,octave,unicode,result", [ (440, 440, 1, False, False, True, "Sa"), (466, 440, 1, False, False, True, "Ri�"), (493, 440, 1, False, False, True, "Ga�"), (880, 440, 1, True, False, True, "S"), (880, 440, 1, True, True, False, "S'"), (880, 440, 1, True, True, True, "Ṡ"), (880, 440, 1, False, True, True, "Ṡa"), (220, 440, 1, False, True, True, "Ṣa"), (740, 440, 1, True, True, True, "N�"), (740, 440, 2, True, True, True, "D₂"), ], ) def test_hz_to_svara_c(freq, Sa, mela, abbr, octave, unicode, result): s = librosa.hz_to_svara_c( freq, Sa=Sa, mela=mela, abbr=abbr, octave=octave, unicode=unicode ) assert s == result @pytest.mark.parametrize( "note,Sa,mela,abbr,octave,unicode,result", [ ("C4", "C4", 1, False, False, True, "Sa"), ("C#4", "C4", 1, False, False, True, "Ri�"), ("D4", "C4", 1, False, False, True, "Ga�"), ("C5", "C4", 1, True, False, True, "S"), ("C5", "C4", 1, True, True, False, "S'"), ("C5", "C4", 1, True, True, True, "Ṡ"), ("C5", "C4", 1, False, True, True, "Ṡa"), ("C3", "C4", 1, False, True, True, "Ṣa"), ("A4", "C4", 1, True, True, True, "N�"), ("A4", "C4", 2, True, True, True, "D₂"), ], ) def test_note_to_svara_c(note, Sa, mela, abbr, octave, unicode, result): s = librosa.note_to_svara_c( note, Sa=Sa, mela=mela, abbr=abbr, octave=octave, unicode=unicode ) assert s == result @pytest.mark.parametrize("value", [np.inf, -np.inf, np.nan]) def test_midi_to_note_empty(value): assert librosa.midi_to_note(value) == "" @pytest.mark.parametrize("value", [np.inf, -np.inf, np.nan]) def test_midi_to_svara_h_empty(value): assert librosa.midi_to_svara_h(value, Sa=60) == "" @pytest.mark.parametrize("value", [np.inf, -np.inf, np.nan]) def test_midi_to_svara_c_empty(value): assert librosa.midi_to_svara_c(value, Sa=60, mela=22) == "" def test_note_to_midi_empty(): assert np.isnan(librosa.note_to_midi("")) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1786462635.232227 librosa-1.0.0/tests/test_core.py0000644000175100017510000026536215236640653016344 0ustar00runnerrunner#!/usr/bin/env python # CREATED:2013-03-08 15:25:18 by Brian McFee # unit tests for librosa core (__init__.py) # from __future__ import print_function # Disable cache import os try: os.environ.pop("LIBROSA_CACHE_DIR") except KeyError: pass import sys import soundfile import librosa import librosa.core import librosa.core.spectrum import glob import numpy as np import scipy.io import scipy.signal import pytest import warnings from unittest import mock from typing import Any, Callable, Union, cast def test_load_soundfile(): fname = os.path.join("tests", "test_audio.ogg") # Load from filename y, sr = librosa.load(fname, sr=None, mono=False) # Load from soundfile object sfo = soundfile.SoundFile(fname) y2, sr2 = librosa.load(sfo, sr=None, mono=False) assert np.allclose(y, y2) assert np.isclose(sr, sr2) @pytest.mark.parametrize("offset", [0.5, 0.7, 1.0, 1.1, 2.0]) @pytest.mark.parametrize("duration", [None, 0.5, 1.0]) @pytest.mark.parametrize("fmt", ["flac", pytest.param("ogg", marks=pytest.mark.xfail(reason="ogg vorbis has problems seeking sometimes", strict=False))]) def test_load_negative_offset(offset, duration, fmt): fname = os.path.join("tests", "test_audio." + fmt) # Load the entire recording y, sr = librosa.load(fname, sr=None, mono=False) # Load the last `offset` seconds of the recording using a negative offset y_end, sr = librosa.load(fname, sr=None, mono=False, offset=-offset, duration=duration) if duration is None or duration >= offset: assert y_end.shape[-1] == int(sr * offset) assert np.allclose(y_end.T, y[..., -y_end.shape[-1]:].T) else: assert y_end.shape[-1] == int(sr * duration) assert np.allclose(y_end, y[..., -int(abs(offset)*sr):-int(abs(offset)*sr)+int(sr*duration)]) @pytest.mark.parametrize("res_type", ["soxr_qq", "soxr_hq", "scipy"]) def test_load_resample(res_type): sr_target = 16000 fn = os.path.join("tests", "test_audio.ogg") y_native, sr = librosa.load(fn, sr=None, res_type=res_type) y2 = librosa.resample(y_native, orig_sr=sr, target_sr=sr_target, res_type=res_type) y, _ = librosa.load(fn, sr=sr_target, res_type=res_type) assert np.allclose(y2, y) def test_segment_load(): sample_len = 2003 fs = 44100 test_file = os.path.join("tests", "test_audio.ogg") y, sr = librosa.load( test_file, sr=None, mono=False, offset=0.0, duration=sample_len / float(fs) ) assert y.shape[-1] == sample_len y2, sr = librosa.load(test_file, sr=None, mono=False) assert np.allclose(y, y2[:, :sample_len]) sample_offset = 2048 y, sr = librosa.load( test_file, sr=None, mono=False, offset=sample_offset / float(fs), duration=1.0 ) assert y.shape[-1] == fs y2, sr = librosa.load(test_file, sr=None, mono=False) assert np.allclose(y, y2[:, sample_offset : sample_offset + fs]) @pytest.fixture(scope="module", params=[22050, 44100]) def resample_audio(request): sr = request.param # Make a tone y = librosa.tone(frequency=882.0, sr=sr, duration=2.0) # Make stereo y = np.vstack([y, y]) return (y, sr) @pytest.fixture(scope="module") def resample_mono(resample_audio): y, sr = resample_audio y = librosa.to_mono(y) return (y, sr) @pytest.mark.parametrize("sr_out", [8000, 22050]) @pytest.mark.parametrize( "res_type", [ "kaiser_best", "kaiser_fast", "scipy", "fft", "polyphase", "linear", "sinc_best", "sinc_fastest", "sinc_medium", "zero_order_hold", "soxr_qq", "soxr_lq", "soxr_mq", "soxr_hq", "soxr_vhq", ], ) @pytest.mark.parametrize("fix", [False, True]) def test_resample_mono(resample_mono, sr_out, res_type, fix): y, sr_in = resample_mono y = librosa.to_mono(y) y2 = librosa.resample( y, orig_sr=sr_in, target_sr=sr_out, res_type=res_type, fix=fix ) # First, check that the audio is valid librosa.util.valid_audio(y2) # If it's a no-op, make sure the signal is untouched if sr_out == sr_in: assert np.allclose(y, y2) # Check buffer contiguity assert y2.flags["C_CONTIGUOUS"] == y.flags["C_CONTIGUOUS"] assert y2.flags["F_CONTIGUOUS"] == y.flags["F_CONTIGUOUS"] # Check that we're within one sample of the target length target_length = y.shape[-1] * sr_out // sr_in assert np.abs(y2.shape[-1] - target_length) <= 1 @pytest.mark.parametrize("sr_out", [8000, 22050]) @pytest.mark.parametrize( "res_type", [ "kaiser_best", "kaiser_fast", "scipy", "fft", "polyphase", "linear", "sinc_best", "sinc_fastest", "sinc_medium", "zero_order_hold", "soxr_qq", "soxr_lq", "soxr_mq", "soxr_hq", "soxr_vhq", ], ) @pytest.mark.parametrize("fix", [False, True]) def test_resample_stereo(resample_audio, sr_out, res_type, fix): y, sr_in = resample_audio y2 = librosa.resample( y, orig_sr=sr_in, target_sr=sr_out, res_type=res_type, fix=fix ) # First, check that the audio is valid librosa.util.valid_audio(y2) assert y2.ndim == y.ndim # If it's a no-op, make sure the signal is untouched if sr_out == sr_in: assert np.allclose(y, y2) # Check that we're within one sample of target_length = y.shape[-1] * sr_out // sr_in target_length = y.shape[-1] * sr_out // sr_in assert np.abs(y2.shape[-1] - target_length) <= 1 @pytest.mark.parametrize( "res_type", [ "fft", "kaiser_best", "kaiser_fast", # "polyphase", # skip this one due to low precision "sinc_best", "sinc_fastest", "sinc_medium", "soxr_lq", "soxr_mq", "soxr_hq", "soxr_vhq", ], ) @pytest.mark.parametrize("sr_out", [11025, 22050, 44100]) def test_resample_scale(resample_mono, res_type, sr_out): y, sr_in = resample_mono y2 = librosa.resample( y, orig_sr=sr_in, target_sr=sr_out, res_type=res_type, scale=True ) # First, check that the audio is valid librosa.util.valid_audio(y2) n_orig = np.sqrt(np.sum(np.abs(y) ** 2)) n_res = np.sqrt(np.sum(np.abs(y2) ** 2)) # If it's a no-op, make sure the signal is untouched assert np.allclose(n_orig, n_res, atol=1e-2), (n_orig, n_res) @pytest.mark.parametrize("sr_in, sr_out", [(100, 100.1), (100.1, 100)]) @pytest.mark.xfail(raises=librosa.ParameterError) def test_resample_poly_float(sr_in, sr_out): y = np.zeros(128) librosa.resample(y, orig_sr=sr_in, target_sr=sr_out, res_type="polyphase") @pytest.mark.parametrize("center", [False, True]) @pytest.mark.parametrize("n_fft", [256, 501]) @pytest.mark.parametrize("window", ["hann", "ones"]) @pytest.mark.parametrize("hop_length", [None, 128]) def test_stft(y_22050, n_fft, window, hop_length, center): D = librosa.stft( y_22050, n_fft=n_fft, window=window, hop_length=hop_length, center=center ) assert D.ndim == 2 assert D.shape[0] == (n_fft // 2 + 1) if hop_length is None: hop_length = n_fft // 4 if center == False: assert D.shape[-1] == 1 + (len(y_22050) - n_fft) // hop_length else: assert D.shape[-1] == 1 + len(y_22050) // hop_length # This is essentially implementing all the STFT computation again, but # much less efficiently than librosa's version window = librosa.filters.get_window(window, n_fft, fftbins=True) if center: y_frames = librosa.util.frame( np.pad(y_22050, n_fft // 2, mode="constant"), frame_length=n_fft, hop_length=hop_length, ) else: y_frames = librosa.util.frame( y_22050, frame_length=n_fft, hop_length=hop_length ) D_direct = scipy.fft.rfft(y_frames * window[:, np.newaxis], axis=0) assert np.allclose(D_direct, D) @pytest.mark.xfail(raises=librosa.ParameterError) def test_stft_toolong_left(): y = np.zeros((128,)) librosa.stft(y, n_fft=2048, center=False) def test_stft_toolong_center(): y = np.zeros((128,)) with pytest.warns(UserWarning): librosa.stft(y, n_fft=2048, center=True) def test_stft_winsizes(): # Test for issue #1095 x = np.zeros(1000000) for power in range(12, 17): N = 2**power H = N // 2 librosa.stft(x, n_fft=N, hop_length=H, win_length=N) @pytest.mark.parametrize("center", [False, True]) @pytest.mark.parametrize( "n_fft, hop_length", [(1023, 128), (1023, 129), (1023, 256), (2048, 512), (2048, 2048)], ) @pytest.mark.parametrize("N", [1024, 2048, 8192]) def test_stft_preallocate(center, n_fft, hop_length, N, rng): # Work in stereo by default y = rng.standard_normal(size=(2, max(N, n_fft))) D1 = librosa.stft(y, center=center, n_fft=n_fft, hop_length=hop_length) out = np.empty_like(D1) D2 = librosa.stft(y, center=center, n_fft=n_fft, hop_length=hop_length, out=out) assert D2 is out assert np.allclose(D1, D2) @pytest.mark.parametrize("center", [False, True]) @pytest.mark.parametrize( "n_fft, hop_length", [(1023, 128), (1023, 129), (1023, 256), (2048, 512), (2048, 2048)], ) @pytest.mark.parametrize("N", [2048]) def test_stft_preallocate_oversize(center, n_fft, hop_length, N, rng): # Work in stereo by default y = rng.standard_normal(size=(2, max(N, n_fft))) D1 = librosa.stft(y, center=center, n_fft=n_fft, hop_length=hop_length) shape = list(D1.shape) shape[-1] *= 2 out = np.empty_like(D1, shape=shape) D2 = librosa.stft(y, center=center, n_fft=n_fft, hop_length=hop_length, out=out) assert np.allclose(D1, D2) assert np.allclose(D1, out[..., : D2.shape[-1]]) @pytest.mark.parametrize("center", [False, True]) @pytest.mark.parametrize( "n_fft, hop_length", [(1023, 128), (1023, 129), (1023, 256), (2048, 512), (2048, 2048)], ) @pytest.mark.parametrize("N", [2048]) @pytest.mark.xfail(raises=librosa.ParameterError) def test_stft_preallocate_undersize(center, n_fft, hop_length, N, rng): # Work in stereo by default y = rng.standard_normal(size=(2, max(N, n_fft))) D1 = librosa.stft(y, center=center, n_fft=n_fft, hop_length=hop_length) shape = list(D1.shape) shape[-1] //= 2 out = np.empty_like(D1, shape=shape) D2 = librosa.stft(y, center=center, n_fft=n_fft, hop_length=hop_length, out=out) @pytest.mark.parametrize("center", [False, True]) @pytest.mark.parametrize( "n_fft, hop_length", [(1023, 128), (1023, 129), (1023, 256), (2048, 512), (2048, 2048)], ) @pytest.mark.parametrize("N", [1024, 2048, 8192]) def test_istft_preallocate(center, n_fft, hop_length, N, rng): y = rng.standard_normal(size=(2, max(N, n_fft))) D = librosa.stft(y, center=center, n_fft=n_fft, hop_length=hop_length) y1 = librosa.istft(D, center=center, n_fft=n_fft, hop_length=hop_length) y2 = np.empty_like(y1) y3 = librosa.istft(D, center=center, n_fft=n_fft, hop_length=hop_length, out=y2) assert y3 is y2 assert np.allclose(y1, y2) # results for FFT bins containing multiple components will be unstable, as when # using higher sampling rates or shorter windows with this test signal @pytest.mark.parametrize("center", [False]) @pytest.mark.parametrize("sr", [256, 512, 2000, 2048]) @pytest.mark.parametrize("n_fft", [128, 255, 256, 512, 1280]) def test___reassign_frequencies(sr, n_fft, center): x = np.linspace(0, 5, 5 * sr, endpoint=False) y = np.sin(17 * x * 2 * np.pi) + np.sin(103 * x * 2 * np.pi) freqs, S = librosa.core.spectrum.__reassign_frequencies( y=y, sr=sr, n_fft=n_fft, hop_length=n_fft, center=center ) S_db = librosa.amplitude_to_db(np.abs(S), ref=np.max) # frequencies should be reassigned to the closest component within 3 Hz # ignore reassigned estimates with low support lower = freqs[(freqs > 0) & (freqs < 60) & (S_db > -30)] assert np.allclose(lower, 17, atol=3) upper = freqs[(freqs >= 60) & (freqs < sr // 2) & (S_db > -30)] assert np.allclose(upper, 103, atol=3) # results for longer windows containing multiple impulses will be unstable @pytest.mark.parametrize("sr", [1, 512, 2048, 22050]) @pytest.mark.parametrize("n_fft", [128, 256, 1024, 2099]) def test___reassign_times(sr, n_fft): y = np.zeros(4096) y[[263, 2633]] = 1 # frames with no energy will have all NaN time reassignments expected_frames = librosa.util.frame(y, frame_length=n_fft, hop_length=n_fft) expected = np.full((n_fft // 2 + 1, expected_frames.shape[1]), np.nan) # find the impulses again; needed if the signal is truncated by framing impulse_indices = np.nonzero(expected_frames.ravel("F"))[0] # find the frames that the impulses should be placed into expected_bins = librosa.samples_to_frames(impulse_indices, hop_length=n_fft) # in each frame that contains an impulse, the energy in every frequency bin # should be reassigned to the original sample time expected_times = librosa.samples_to_time(impulse_indices, sr=sr) expected[:, expected_bins] = np.tile(expected_times, (n_fft // 2 + 1, 1)) times, S = librosa.core.spectrum.__reassign_times( y=y, sr=sr, n_fft=n_fft, hop_length=n_fft, center=False ) # times should be reassigned within 0.5% of the window duration assert np.allclose(times, expected, atol=0.005 * n_fft / sr, equal_nan=True) def test___reassign_times_center(): y = np.zeros(4096) y[2049] = 1 sr = 4000 n_fft = 2048 times, S = librosa.core.spectrum.__reassign_times( y=y, sr=sr, hop_length=n_fft, win_length=n_fft, center=True ) expected = np.full_like(times, np.nan) expected[:, 1] = 2049 / float(sr) assert np.allclose(times, expected, atol=0.005 * n_fft / sr, equal_nan=True) @pytest.mark.parametrize("clip", [False, True]) @mock.patch("librosa.core.spectrum.__reassign_times") @mock.patch("librosa.core.spectrum.__reassign_frequencies") def test_reassigned_spectrogram_clip( mock_reassign_frequencies, mock_reassign_times, clip ): mock_freqs = np.ones((5, 17)) mock_freqs[0, 0] = -1 mock_freqs[0, 1] = 33 mock_times = np.ones((5, 17)) mock_times[1, 0] = -1 mock_times[1, 1] = 3 mock_mags = np.ones((5, 17)) mock_reassign_frequencies.return_value = mock_freqs, mock_mags mock_reassign_times.return_value = mock_times, mock_mags freqs, times, mags = librosa.reassigned_spectrogram( y=np.zeros(128), sr=64, n_fft=8, hop_length=8, clip=clip ) # freqs and times outside the spectrogram bounds if clip: assert freqs[0, 0] == 0 assert freqs[0, 1] == 32 assert times[1, 0] == 0 assert times[1, 1] == 2 else: assert freqs[0, 0] == -1 assert freqs[0, 1] == 33 assert times[1, 0] == -1 assert times[1, 1] == 3 assert freqs[2, 0] == 1 assert times[2, 1] == 1 @pytest.mark.parametrize("ref_power", [0.0, 1e-6, np.max]) @mock.patch("librosa.core.spectrum.__reassign_times") @mock.patch("librosa.core.spectrum.__reassign_frequencies") def test_reassigned_spectrogram_ref_power( mock_reassign_frequencies, mock_reassign_times, ref_power ): mock_freqs = np.ones((5, 17)) mock_times = np.ones((5, 17)) mock_mags = np.ones((5, 17)) mock_mags[2, 0] = 0 mock_mags[2, 1] = 0.1 mock_reassign_frequencies.return_value = mock_freqs, mock_mags mock_reassign_times.return_value = mock_times, mock_mags freqs, times, mags = librosa.reassigned_spectrogram( y=np.zeros(128), sr=64, n_fft=8, hop_length=8, ref_power=ref_power ) if ref_power is np.max: assert np.isnan(freqs[2, 0]) assert np.isnan(freqs[2, 1]) assert np.isnan(times[2, 0]) assert np.isnan(times[2, 1]) elif ref_power == 1e-6: assert np.isnan(freqs[2, 0]) assert freqs[2, 1] == 1 assert np.isnan(times[2, 0]) assert times[2, 1] == 1 elif ref_power == 0: assert freqs[2, 0] == 1 assert freqs[2, 1] == 1 assert times[2, 0] == 1 assert times[2, 1] == 1 assert freqs[2, 2] == 1 assert times[2, 2] == 1 @pytest.mark.parametrize("fill_nan", [False, True]) @mock.patch("librosa.core.spectrum.__reassign_times") @mock.patch("librosa.core.spectrum.__reassign_frequencies") def test_reassigned_spectrogram_fill_nan( mock_reassign_frequencies, mock_reassign_times, fill_nan ): mock_freqs = np.ones((5, 17)) mock_times = np.ones((5, 17)) # mock divide by zero mock_freqs[3, 0] = np.nan mock_times[3, 0] = np.nan # mock below default ref_power threshold (<1e-6) mock_mags = np.ones((5, 17)) mock_mags[3, 1] = 0 mock_reassign_frequencies.return_value = mock_freqs, mock_mags mock_reassign_times.return_value = mock_times, mock_mags freqs, times, mags = librosa.reassigned_spectrogram( y=np.zeros(128), sr=64, n_fft=8, hop_length=8, center=False, fill_nan=fill_nan ) if fill_nan: # originally NaN due to divide-by-zero assert freqs[3, 0] == 24 assert times[3, 0] == 0.0625 # originally NaN due to low power assert freqs[3, 1] == 24 assert times[3, 1] == 0.1875 else: assert np.isnan(freqs[3, 0]) assert np.isnan(times[3, 0]) assert np.isnan(freqs[3, 1]) assert np.isnan(times[3, 1]) assert mags[3, 1] == 0 assert freqs[3, 2] == 1 assert times[3, 2] == 1 @pytest.mark.parametrize("center", [False, True]) @pytest.mark.parametrize("reassign_times", [False, True]) @pytest.mark.parametrize("reassign_frequencies", [False, True]) def test_reassigned_spectrogram_flags(reassign_frequencies, reassign_times, center): if not reassign_frequencies and not reassign_times: return freqs, times, mags = librosa.reassigned_spectrogram( y=np.zeros(2048), center=center, reassign_frequencies=reassign_frequencies, reassign_times=reassign_times, ) if reassign_frequencies: assert np.all(np.isnan(freqs)) else: bin_freqs = librosa.fft_frequencies() assert np.array_equiv(freqs, bin_freqs[:, np.newaxis]) if reassign_times: assert np.all(np.isnan(times)) else: frame_times = librosa.frames_to_time(np.arange(freqs.shape[1])) if not center: frame_times = frame_times + (2048.0 / 22050.0 / 2.0) assert np.array_equiv(times, frame_times[np.newaxis, :]) def test_reassigned_spectrogram_parameters(): with pytest.raises(librosa.ParameterError): freqs, times, mags = librosa.reassigned_spectrogram( y=np.zeros(2048), ref_power=-1 ) with pytest.raises(librosa.ParameterError): freqs, times, mags = librosa.reassigned_spectrogram( y=np.zeros(2048), reassign_frequencies=False, reassign_times=False ) def test_salience_basecase(y_22050): sr = 22050 S = np.abs(librosa.stft(y_22050)) freqs = librosa.core.fft_frequencies(sr=sr) harms = [1] weights = [1.0] S_sal = librosa.core.salience( S, freqs=freqs, harmonics=harms, weights=weights, filter_peaks=False, kind="quadratic", ) assert np.allclose(S_sal, S) def test_salience_basecase2(y_22050): sr = 22050 S = np.abs(librosa.stft(y_22050)) freqs = librosa.core.fft_frequencies(sr=sr) harms = [1, 0.5, 2.0] weights = [1.0, 0.0, 0.0] S_sal = librosa.core.salience( S, freqs=freqs, harmonics=harms, weights=weights, filter_peaks=False, kind="quadratic", ) assert np.allclose(S_sal, S) def test_salience_defaults(): S = np.array([[0.1, 0.5, 0.0], [0.2, 1.2, 1.2], [0.0, 0.7, 0.3], [1.3, 3.2, 0.8]]) freqs = np.array([50.0, 100.0, 200.0, 400.0]) harms = [0.5, 1, 2] actual = librosa.core.salience( S, freqs=freqs, harmonics=harms, kind="quadratic", fill_value=0.0 ) expected = ( np.array([[0.0, 0.0, 0.0], [0.3, 2.4, 1.5], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]) / 3.0 ) assert np.allclose(expected, actual) def test_salience_weights(): S = np.array([[0.1, 0.5, 0.0], [0.2, 1.2, 1.2], [0.0, 0.7, 0.3], [1.3, 3.2, 0.8]]) freqs = np.array([50.0, 100.0, 200.0, 400.0]) harms = [0.5, 1, 2] weights = [1.0, 1.0, 1.0] actual = librosa.core.salience( S, freqs=freqs, harmonics=harms, weights=weights, kind="quadratic", fill_value=0.0, ) expected = ( np.array([[0.0, 0.0, 0.0], [0.3, 2.4, 1.5], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]) / 3.0 ) assert np.allclose(expected, actual) def test_salience_no_peak_filter(): S = np.array([[0.1, 0.5, 0.0], [0.2, 1.2, 1.2], [0.0, 0.7, 0.3], [1.3, 3.2, 0.8]]) freqs = np.array([50.0, 100.0, 200.0, 400.0]) harms = [0.5, 1, 2] weights = [1.0, 1.0, 1.0] actual = librosa.core.salience( S, freqs=freqs, harmonics=harms, weights=weights, filter_peaks=False, kind="quadratic", ) expected = ( np.array([[0.3, 1.7, 1.2], [0.3, 2.4, 1.5], [1.5, 5.1, 2.3], [1.3, 3.9, 1.1]]) / 3.0 ) assert np.allclose(expected, actual) def test_salience_aggregate(): S = np.array([[0.1, 0.5, 0.0], [0.2, 1.2, 1.2], [0.0, 0.7, 0.3], [1.3, 3.2, 0.8]]) freqs = np.array([50.0, 100.0, 200.0, 400.0]) harms = [0.5, 1, 2] weights = [1.0, 1.0, 1.0] actual = librosa.core.salience( S, freqs=freqs, harmonics=harms, weights=weights, aggregate=np.max, kind="quadratic", fill_value=0.0, ) expected = np.array( [[0.0, 0.0, 0.0], [0.2, 1.2, 1.2], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]] ) assert np.allclose(expected, actual) @pytest.fixture(scope="module") def y_22050(): sr = 22050 y = librosa.chirp(fmin=55, fmax=55 * 2**7, sr=sr, duration=4.0) return y def test_magphase(y_22050): D = librosa.stft(y_22050) S, P = librosa.magphase(D) assert S.dtype is y_22050.dtype # float assert P.dtype is D.dtype # complex assert np.allclose(np.abs(P), 1.0) assert np.allclose(S * P, D) def test_magphase_zero(): D = np.zeros((128, 128), dtype=np.complex64) S, P = librosa.magphase(D) assert S.dtype is np.dtype("float32") assert P.dtype is np.dtype("complex64") assert np.allclose(S, 0) assert np.allclose(P, 1 + 0j) def test_magphase_denormalized(): D = 1.0e-42j * np.ones((128, 128), dtype=np.complex64) S, P = librosa.magphase(D) assert S.dtype is np.dtype("float32") assert P.dtype is np.dtype("complex64") assert np.allclose(S, 1.0e-42) assert np.allclose(P, 0 + 1j) def test_magphase_real(): D = np.array([[-1.0, -0.0], [0.0, 1.0]], dtype=np.float64) S, P = librosa.magphase(D) assert S.dtype is np.dtype("float64") assert P.dtype is np.dtype("complex128") assert np.allclose(S, np.array([[1.0, 0.0], [0.0, 1.0]])) assert np.allclose( [ [P[0, 0], P[0, 1] ** 2], # negative zero can have phase +1 or -1 [P[1, 0], P[1, 1]], ], np.array([[-1 + 0j, 1 + 0j], [1 + 0j, 1 + 0j]]), ) @pytest.fixture(scope="module", params=[22050, 44100]) def y_chirp_istft(request): sr = request.param return (librosa.chirp(fmin=32, fmax=8192, sr=sr, duration=2.0), sr) @pytest.mark.parametrize("n_fft", [1024, 1025, 2048, 4096]) @pytest.mark.parametrize("window", ["hann", "blackmanharris"]) @pytest.mark.parametrize("hop_length", [128, 256, 512]) def test_istft_reconstruction(y_chirp_istft, n_fft, hop_length, window): x, sr = y_chirp_istft S = librosa.core.stft(x, n_fft=n_fft, hop_length=hop_length, window=window) x_reconstructed = librosa.core.istft( S, hop_length=hop_length, window=window, n_fft=n_fft, length=len(x) ) # NaN/Inf/-Inf should not happen assert np.all(np.isfinite(x_reconstructed)) # should be almost approximately reconstructed assert np.allclose(x, x_reconstructed, atol=1e-6) @pytest.mark.parametrize("offset", [0, 1, 2]) @pytest.mark.parametrize("duration", [None, 0, 0.5, 1, 2]) @pytest.mark.parametrize("mono", [False, True]) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) @pytest.mark.parametrize("filename", [os.path.join("tests", "test_audio.ogg")]) def test_load_options(filename, offset, duration, mono, dtype): y, sr = librosa.load( filename, mono=mono, offset=offset, duration=duration, dtype=dtype ) if duration is not None: assert np.allclose(y.shape[-1], int(sr * duration)) if mono: assert y.ndim == 1 else: # This test file is stereo, so y.ndim should be 2 assert y.ndim == 2 # Check the dtype assert np.issubdtype(y.dtype, dtype) assert np.issubdtype(dtype, y.dtype) @pytest.mark.parametrize("key", ["trumpet"]) @pytest.mark.parametrize("sr", [8000, 22050, 44100]) @pytest.mark.parametrize("mono", [False, True]) @pytest.mark.network def test_loadx(key, sr, mono): y, sr_loaded = librosa.loadx(key, sr=sr, mono=mono) if sr is not None: assert sr_loaded == sr if mono: assert y.ndim == 1 else: assert y.ndim == 2 # Load the same file using the old way hq = True if mono and (sr is not None and sr <= 22050): hq = False y_old, sr_old = librosa.load(librosa.ex(key, hq=hq), sr=sr, mono=mono) assert sr_loaded == sr_old assert np.allclose(y_old, y) @pytest.mark.xfail(raises=librosa.ParameterError) def test_loadx_fail(): librosa.loadx("not an example.ogg") @pytest.mark.parametrize("sr", [8000, 11025]) @pytest.mark.parametrize("dur", [0.25, 1.0]) def test_get_duration_buffer(sr, dur): y = np.zeros(int(sr * dur)) dur_est = librosa.get_duration(y=y, sr=sr) assert np.isclose(dur_est, dur, atol=1e-4) @pytest.mark.parametrize("sr", [8000, 11025]) @pytest.mark.parametrize("dur", [0.25, 1.0]) @pytest.mark.parametrize("center", [False, True]) @pytest.mark.parametrize("n_fft", [512, 2048]) @pytest.mark.parametrize("hop_length", [256, 512]) def test_get_duration_specgram(sr, dur, n_fft, hop_length, center): z = np.zeros(int(sr * dur)) S = librosa.util.frame(z, frame_length=(n_fft // 2 + 1), hop_length=hop_length) dur_est = librosa.get_duration( S=S, sr=sr, n_fft=n_fft, hop_length=hop_length, center=center ) if center: # If we're assuming centering, estimated samples lose 2 * int(n_fft//2) assert np.abs(dur_est + 2 * int(n_fft // 2) / sr - dur) <= n_fft / sr else: # If we're not assuming centering, then duration should be off by at most n_fft assert np.abs(dur_est - dur) <= n_fft / sr def test_get_duration_ogg(): filename = os.path.join("tests", "test_audio.ogg") true_duration = 5.19 duration_fn = librosa.get_duration(path=filename) y, sr = librosa.load(filename, sr=None) duration_y = librosa.get_duration(y=y, sr=sr) # mp3 duration at low sampling rate isn't too reliable assert np.allclose(duration_fn, duration_y, atol=1e-1) assert np.allclose(duration_fn, true_duration, atol=1e-1) @pytest.mark.xfail(raises=librosa.ParameterError) def test_get_duration_fail(): librosa.get_duration(y=None, S=None, path=None) @pytest.mark.parametrize("real", [True, False]) @pytest.mark.parametrize("axis", [0, 1, -1]) @pytest.mark.parametrize("max_size", [None, 128, 256, 512]) def test_autocorrelate(real, axis, max_size, rng): y = rng.standard_normal(size=(256, 2)) if not real: y = np.exp(1.0j * y) truth = [ np.asarray( [ scipy.signal.fftconvolve(yi, yi[::-1].conj(), mode="full")[ len(yi) - 1 : ] for yi in y.T ] ).T, np.asarray( [ scipy.signal.fftconvolve(yi, yi[::-1].conj(), mode="full")[ len(yi) - 1 : ] for yi in y ] ), ] ac = librosa.autocorrelate(y, max_size=max_size, axis=axis) my_slice = [slice(None)] * truth[axis].ndim if max_size is not None and max_size <= y.shape[axis]: my_slice[axis] = slice(min(max_size, y.shape[axis])) if not np.iscomplexobj(y): assert not np.iscomplexobj(ac) assert np.allclose(ac, truth[axis][tuple(my_slice)]) @pytest.fixture(scope="module") def y_chirp_lpc(): y = librosa.chirp(fmin=50, fmax=500, sr=22050, duration=2.0).astype(np.float64) return y @pytest.mark.parametrize("order", [4, 8, 12]) def test_lpc_order(y_chirp_lpc, order): # Test LPC predictions by using them as feed-forward filter coefficients # Use high-precision float to avoid numerical issues a = librosa.lpc(y_chirp_lpc, order=order) b = np.hstack([[0], -1 * a[1:]]) y_hat = scipy.signal.lfilter(b, [1], y_chirp_lpc) assert np.allclose(y_hat[order:], y_chirp_lpc[order:], atol=1e-3) @pytest.mark.parametrize("dtype", [np.float64, np.float32]) def test_lpc_simple(dtype, rng): n = 5000 est_a = np.zeros((n, 6), dtype=dtype) truth_a = np.array([1, 0.5, 0.4, 0.3, 0.2, 0.1], dtype=dtype) for i in range(n): noise = rng.standard_normal(size=1000).astype(dtype) filtered = scipy.signal.lfilter(dtype([1]), truth_a, noise) est_a[i, :] = librosa.lpc(filtered, order=5) assert dtype == est_a.dtype assert np.allclose(truth_a, np.mean(est_a, axis=0), rtol=0, atol=1e-2) @pytest.mark.parametrize( "y", [np.arange(5.0), np.ones((2, 5), dtype=float), np.ones((3, 5), dtype=float)], ids=["mono", "stereo", "multi"], ) def test_to_mono(y): y_mono = librosa.to_mono(y) assert y_mono.ndim == 1 assert len(y_mono) == y.shape[-1] if y.ndim == 1: assert np.allclose(y, y_mono) @pytest.mark.parametrize("norm", [False, True]) @pytest.mark.parametrize("nchannels", [1, 2, 3]) def test_to_mono_norm(norm, nchannels): y = np.ones((nchannels, 5)) y_mono = librosa.to_mono(y, norm=norm) assert y_mono.ndim == 1 assert len(y_mono) == y.shape[-1] if norm: assert np.allclose(y_mono, 1) else: assert np.allclose(y_mono, nchannels) @pytest.mark.xfail(raises=librosa.ParameterError) def test_to_mono_empty(): librosa.to_mono() def test_to_mono_args(rng): y = rng.standard_normal(size=(3, 10)) y_mono1 = librosa.to_mono(y) y_mono2 = librosa.to_mono(y[0], y[1], y[2]) assert np.allclose(y_mono1, y_mono2) @pytest.mark.parametrize( "y", [np.ones((2, 10)), np.ones((2, 3, 10)), np.ones((2, 3, 4, 10))], ids=["2d", "3d", "4d"], ) @pytest.mark.parametrize( "norm", [False, True], ) def test_to_mono_multi(y, norm): y_mono = librosa.to_mono(y, norm=norm) assert y_mono.ndim == 1 assert len(y_mono) == y.shape[-1] if norm: # If we normalize, then everything should be 1 assert np.allclose(y_mono, 1) else: # If we don't normalize, then the sum will be the product # of the number of leading channels assert np.allclose(y_mono, np.prod(y.shape[:-1])) @pytest.mark.parametrize("pad", [False, True]) def test_to_mono_pad(pad): y1 = np.ones((2, 10)) y2 = np.zeros( 5, ) y_mono = librosa.to_mono(y1, y2, pad=pad) if pad: assert y_mono.shape == (10,) else: assert y_mono.shape == (5,) @pytest.mark.xfail(raises=librosa.ParameterError) def test_to_stereo_fail(): librosa.to_stereo(left=None, right=None) @pytest.mark.parametrize("downmix", [False, True]) def test_to_stereo_single(downmix): y = np.ones((10,)) y_stereo = librosa.to_stereo(left=y, right=None, downmix=downmix) assert y_stereo.shape == (2, 10) # Both channels should be the same assert np.allclose(y_stereo[0], y) assert np.allclose(y_stereo[1], 0) y_stereo = librosa.to_stereo(left=None, right=y, downmix=downmix) assert y_stereo.shape == (2, 10) # Both channels should be the same assert np.allclose(y_stereo[0], 0) assert np.allclose(y_stereo[1], y) @pytest.mark.parametrize("downmix", [False, True]) @pytest.mark.parametrize("norm", [False, True]) def test_to_stereo_downmix(downmix, norm): y = np.ones((2, 10)) y[1] *= -1 y_stereo = librosa.to_stereo(left=y, right=y, downmix=downmix, norm=norm) assert y_stereo.shape == (2, 10) if downmix: # If downmixing, then both channels should cancel out assert np.allclose(y_stereo[0], 0) assert np.allclose(y_stereo[1], 0) else: # If not downmixing, then both signals are mixed but channels stay separate if norm: assert np.allclose(y_stereo[0], y[0]) assert np.allclose(y_stereo[1], y[1]) else: assert np.allclose(y_stereo[0], 2 * y[0]) assert np.allclose(y_stereo[1], 2 * y[1]) def test_to_stereo_mixed(): y1 = np.ones((10,)) y2 = np.zeros((2, 10)) y_stereo = librosa.to_stereo(left=y1, right=y2) assert y_stereo.shape == (2, 10) # Left channel should be y1, right channel should be y2 assert np.allclose(y_stereo[0], y1) assert np.allclose(y_stereo[1], y2) # Now try it in reverse y_stereo = librosa.to_stereo(left=y2, right=y1) assert y_stereo.shape == (2, 10) # Left channel should be y1, right channel should be y2 assert np.allclose(y_stereo[0], y2) assert np.allclose(y_stereo[1], y1) def test_to_stereo_downmix_mismatch(): y1 = np.ones((3, 10)) y2 = np.ones((2, 10)) with pytest.raises(librosa.ParameterError): librosa.to_stereo(left=y1, right=y2, downmix=False) with pytest.raises(librosa.ParameterError): librosa.to_stereo(left=y2, right=y1, downmix=False) @pytest.mark.parametrize("pad", [False, True]) def test_to_stereo_pad(pad): y1 = np.ones((10,)) y2 = np.zeros((5,)) y_stereo = librosa.to_stereo(left=y1, right=y2, pad=pad) if pad: assert y_stereo.shape == (2, 10) else: assert y_stereo.shape == (2, 5) @pytest.mark.xfail(raises=librosa.ParameterError) def test_to_multi_noinput(): librosa.to_multi() @pytest.mark.xfail(raises=librosa.ParameterError) def test_to_multi_mismatch(): y1 = np.ones((3, 10)) y2 = np.ones((2, 10)) librosa.to_multi(y1, y2, downmix=False) @pytest.mark.parametrize("pad", [False, True]) def test_to_multi_pad(pad): y1 = np.ones(5) y2 = np.zeros(10) y3 = np.ones(15) y_multi = librosa.to_multi(y1, y2, y3, pad=pad) if pad: assert y_multi.shape == (3, 15) else: assert y_multi.shape == (3, 5) assert np.allclose(y_multi[0], y1) assert np.allclose(y_multi[1], y2[:5]) assert np.allclose(y_multi[2], y3[:5]) @pytest.mark.parametrize("downmix", [False, True]) @pytest.mark.parametrize("norm", [False, True]) def test_to_multi_layout(downmix, norm): y = np.ones((2, 4, 10)) y_multi = librosa.to_multi(y, 2 * y, 3 * y, downmix=downmix, norm=norm) if downmix: # Three inputs that are each downmixed and stacked assert y_multi.shape == (3, 10) if norm: assert np.allclose(y_multi[0], 1) assert np.allclose(y_multi[1], 2) assert np.allclose(y_multi[2], 3) else: assert np.allclose(y_multi[0], 8) assert np.allclose(y_multi[1], 16) assert np.allclose(y_multi[2], 24) else: # Three inputs, but we're not downmixing, so they all get combined assert y_multi.shape == (2, 4, 10) if norm: # averaged values: (1 + 2 + 3)/3 = 6/3 = 2 assert np.allclose(y_multi, 2) else: # summed values: 1 + 2 + 3 = 6 assert np.allclose(y_multi, 6) @pytest.mark.parametrize("norm", [False, True]) @pytest.mark.parametrize( "signals", [(np.ones((2, 10)),), (np.arange(10.0),), (np.ones(10), np.zeros(10))], ids=["multichannel", "mono", "two-signals"], ) def test_to_mono_out(signals, norm): expected = librosa.to_mono(*signals, norm=norm) # np.empty (not zeros) verifies the buffer is zero-filled before accumulation out = np.empty(expected.shape, dtype=expected.dtype) y_mono = librosa.to_mono(*signals, norm=norm, out=out) assert y_mono is out assert np.allclose(y_mono, expected) @pytest.mark.parametrize("downmix", [False, True]) def test_to_stereo_out(downmix): left = np.ones((10,)) right = np.zeros((10,)) expected = librosa.to_stereo(left=left, right=right, downmix=downmix) out = np.empty(expected.shape, dtype=expected.dtype) y_stereo = librosa.to_stereo(left=left, right=right, downmix=downmix, out=out) assert y_stereo is out assert np.allclose(y_stereo, expected) @pytest.mark.parametrize("downmix", [False, True]) def test_to_multi_out(downmix): y = np.ones((2, 10)) expected = librosa.to_multi(y, 2 * y, downmix=downmix) out = np.empty(expected.shape, dtype=expected.dtype) y_multi = librosa.to_multi(y, 2 * y, downmix=downmix, out=out) assert y_multi is out assert np.allclose(y_multi, expected) @pytest.mark.parametrize( "out", [np.empty(11), np.empty((2, 10)), np.empty(10, dtype=np.float32)], ids=["bad-length", "bad-ndim", "bad-dtype"], ) def test_to_mono_out_invalid(out): # y is float64 with length 10, so out must be shape (10,) and at least # as precise as float64 (float32 is too narrow and is rejected) y = np.ones((2, 10)) with pytest.raises(librosa.ParameterError): librosa.to_mono(y, out=out) def test_to_mono_out_wider_dtype(): # A higher-precision out= buffer is accepted: the caller's explicit # precision takes precedence over the dtype inferred from the input y = np.ones(10, dtype=np.float32) out = np.empty(10, dtype=np.float64) result = librosa.to_mono(y, out=out) assert result is out assert result.dtype == np.float64 assert np.allclose(result, 1.0) @pytest.mark.parametrize("combiner", ["stereo", "multi"]) def test_to_downmix_out_mixed_dtype(combiner): # Inputs with differing dtypes promote to a wider output dtype; the # downmix pass-through must still fill an out= buffer of that dtype # without a dtype mismatch (regression guard for the internal to_mono # calls in to_stereo/to_multi). a = np.ones(10, dtype=np.float32) b = np.ones(10, dtype=np.float64) if combiner == "stereo": expected = librosa.to_stereo(left=a, right=b, downmix=True) out = np.empty(expected.shape, dtype=expected.dtype) result = librosa.to_stereo(left=a, right=b, downmix=True, out=out) else: expected = librosa.to_multi(a, b, downmix=True) out = np.empty(expected.shape, dtype=expected.dtype) result = librosa.to_multi(a, b, downmix=True, out=out) assert result is out assert np.allclose(result, expected) @pytest.mark.parametrize("data", [np.cos(np.arange(32))]) @pytest.mark.parametrize("threshold", [0, 1e-10]) @pytest.mark.parametrize("ref_magnitude", [None, 0.1, np.max]) @pytest.mark.parametrize("pad", [False, True]) @pytest.mark.parametrize("zp", [False, True]) def test_zero_crossings(data, threshold, ref_magnitude, pad, zp): zc = librosa.zero_crossings( y=data, threshold=threshold, ref_magnitude=ref_magnitude, pad=pad, zero_pos=zp ) idx = np.flatnonzero(zc) if pad: idx = idx[1:] for i in idx: assert np.sign(data[i]) != np.sign(data[i - 1]) @pytest.mark.parametrize("resolution", [1e-2, 1e-3]) @pytest.mark.parametrize("tuning", [-0.5, -0.375, -0.25, 0.0, 0.25, 0.375]) @pytest.mark.parametrize("bins_per_octave", [12]) def test_pitch_tuning(resolution, bins_per_octave, tuning): hz = librosa.midi_to_hz(tuning + np.arange(128)) est_tuning = librosa.pitch_tuning( hz, resolution=resolution, bins_per_octave=bins_per_octave ) assert np.abs(tuning - est_tuning) <= resolution @pytest.fixture(params=[2048, 4096], scope="module") def pip_nfft(request): return request.param @pytest.fixture(params=[None, 4, 2], scope="module") def pip_hop(request, pip_nfft): if request.param is None: return None else: return pip_nfft // request.param @pytest.fixture(scope="module") def pip_spec(y_22050, pip_nfft, pip_hop): return np.abs(librosa.stft(y_22050, n_fft=pip_nfft, hop_length=pip_hop)) @pytest.mark.parametrize("fmin", [0, 100]) @pytest.mark.parametrize("fmax", [4000, 8000, 11025]) @pytest.mark.parametrize("threshold", [0.1, 0.2, 0.5]) @pytest.mark.parametrize("ref", [None, 1.0, np.max]) def test_piptrack_properties(pip_spec, pip_nfft, pip_hop, fmin, fmax, threshold, ref): n_fft = pip_nfft hop_length = pip_hop S = pip_spec pitches, mags = librosa.core.piptrack( S=S, n_fft=n_fft, hop_length=hop_length, fmin=fmin, fmax=fmax, threshold=threshold, ref=ref, ) # Shape tests assert S.shape == pitches.shape assert S.shape == mags.shape # Make sure all magnitudes are positive assert np.all(mags >= 0) # Check the frequency estimates for bins with non-zero magnitude idx = mags > 0 assert np.all(pitches[idx] >= fmin) assert np.all(pitches[idx] <= fmax) # And everywhere else, pitch should be 0 assert np.all(pitches[~idx] == 0) def test_piptrack_errors(): np.seterr(divide="raise") pitches, mags = librosa.piptrack( y=None, sr=22050, S=np.asarray([[1.0, 0.0, 0.0]]).T, n_fft=4096, hop_length=None, fmin=150, fmax=4000, threshold=0.1, ) @pytest.mark.parametrize("freq", [110, 220, 440, 880]) def test_yin_tone(freq): y = librosa.tone(freq, duration=1.0) f0 = librosa.yin(y, fmin=110, fmax=880, center=False) assert np.allclose(np.log2(f0), np.log2(freq), rtol=0, atol=1e-2) def test_yin_chirp(): # test yin on a chirp, using output from the vamp plugin as ground truth y = librosa.chirp(fmin=220, fmax=640, duration=1.0) f0 = librosa.yin( y, fmin=110, fmax=880, center=False, frame_length=1024, hop_length=512 ) # adjust frames to the removal of win_length from yin f0 = f0[:-2] target_f0 = np.load(os.path.join("tests", "pitch-yin.npy")) assert np.allclose(np.log2(f0), np.log2(target_f0), rtol=0, atol=1e-2) def test_yin_chirp_instant(): # test yin on a chirp, using frame-wise instantaneous frequency as ground truth sr = 22050 chirp_min, chirp_max = 220, 640 t = np.arange(sr) / sr f = chirp_min * (chirp_max / chirp_min) ** t fl = 2048 hl = 512 y = librosa.chirp(fmin=chirp_min, fmax=chirp_max, sr=sr, duration=1.0, linear=False) target_f0 = librosa.util.frame(f, frame_length=fl, hop_length=hl).mean(axis=0) f0 = librosa.yin( y, fmin=110, fmax=880, sr=sr, frame_length=fl, hop_length=hl, center=False ) assert np.allclose(np.log2(f0), np.log2(target_f0), rtol=0, atol=1e-2) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize( "fmin,fmax,frame_length", [ (None, None, 2048), # neither (110, None, 2048), # no fmax (None, 880, 2048), # no fmin (-1, 440, 2048), # Negative fmin (440, 220, 2048), # fmin > fmax (440, 16000, 2048), # fmax > nyquist (10, 21, 2048), # sr / fmin >= frame_length - 1 ], ) def test_yin_fail(fmin, fmax, frame_length): y = librosa.tone(110, duration=1.0) librosa.yin(y, fmin=fmin, fmax=fmax, frame_length=frame_length) def test_yin_warn(): y = librosa.tone(110, duration=1.0) # sr / fmin >= frame_length // 2 with pytest.warns(UserWarning, match="two periods"): librosa.yin(y, fmin=20, fmax=1000) @pytest.mark.parametrize("freq", [110, 220, 440, 880]) def test_pyin_tone(freq): y = librosa.tone(freq, duration=1.0) f0, _, _ = librosa.pyin(y, fmin=110, fmax=1000, center=False) assert np.allclose(np.log2(f0), np.log2(freq), rtol=0, atol=1e-2) def test_pyin_multi(): y = np.stack([librosa.tone(440, duration=1.0), librosa.tone(560, duration=1.0)]) # Taper the signal h = librosa.filters.get_window("triangle", y.shape[-1]) # Filter it y = y * h[np.newaxis, :] # Disable nans so we can use allclose checks fall, vall, vpall = librosa.pyin(y, fmin=100, fmax=1000, center=False, fill_na=-1) f0, v0, vp0 = librosa.pyin(y[0], fmin=100, fmax=1000, center=False, fill_na=-1) f1, v1, vp1 = librosa.pyin(y[1], fmin=100, fmax=1000, center=False, fill_na=-1) assert np.allclose(fall[0], f0) assert np.allclose(fall[1], f1) assert np.allclose(vall[0], v0) assert np.allclose(vall[1], v1) assert np.allclose(vpall[0], vp0) assert np.allclose(vpall[1], vp1) @pytest.mark.skipif( sys.platform == "darwin", reason="Skip on OSX due to openblas issue" ) def test_pyin_multi_center(): # Note: this test has issues on OSX with libopenblas 0.3.26, # so we disable it for now. We may re-enable it some time in the future. y = np.stack([librosa.tone(440, duration=1.0), librosa.tone(560, duration=1.0)]) # Taper the signal h = librosa.filters.get_window("triangle", y.shape[-1]) # Filter it y = y * h[np.newaxis, :] fleft, vleft, vpleft = librosa.pyin(y, fmin=100, fmax=1000, center=False) fc, vc, vpc = librosa.pyin(y, fmin=100, fmax=1000, center=True) # Centering will pad by half a frame on either side # hop length is one quarter frame # ==> match on 2:-2 # Loosening tolerances here to account for platform differences assert np.allclose(vpleft, vpc[..., 2:-2]) assert np.allclose(vleft, vc[..., 2:-2]) assert np.allclose(fleft, fc[..., 2:-2], equal_nan=True), np.max( np.abs(fleft - fc[..., 2:-2]) ) def test_pyin_chirp(): # test yin on a chirp, using output from the vamp plugin as ground truth y = librosa.chirp(fmin=220, fmax=640, duration=1.0) y = np.pad(y, (22050,)) # default values as set in https://code.soundsoftware.ac.uk/projects/pyin/repository/ f0, voiced_flag, _ = librosa.pyin( y, fmin=60, fmax=900, center=False, frame_length=1024, hop_length=512, resolution=0.2, ) # adjust frames to the removal of win_length from yin f0 = f0[:-2] voiced_flag = voiced_flag[:-2] target_f0 = np.load(os.path.join("tests", "pitch-pyin.npy")) # test if correct frames are voiced assert np.array_equal(voiced_flag, target_f0 > 0) # test voiced frames are within one cent of the target assert np.allclose( np.log2(f0[voiced_flag]), np.log2(target_f0[target_f0 > 0]), rtol=0, atol=1e-2 ) def test_pyin_chirp_instant(): # test pyin on a chirp, using frame-wise instantaneous frequency as ground truth sr = 22050 chirp_min, chirp_max = 220, 640 t = np.arange(sr) / sr f = chirp_min * (chirp_max / chirp_min) ** t f = np.pad(f, (sr,)) fl = 2048 hl = 512 # Note: this raises warnings on the empty frames target_f0 = librosa.util.frame(f, frame_length=fl, hop_length=hl) with pytest.warns(RuntimeWarning): target_f0 = target_f0.mean(axis=0, where=target_f0 > 0) y = librosa.chirp(fmin=chirp_min, fmax=chirp_max, sr=sr, duration=1.0, linear=False) y = np.pad(y, (sr,)) f0, voiced_flag, _ = librosa.pyin( y, fmin=110, fmax=880, frame_length=fl, hop_length=hl, center=False ) # test if correct frames are voiced assert np.array_equal(voiced_flag, target_f0 > 0) # test voiced frames are within one cent of the target cents = np.log2(f0[voiced_flag]) target_cents = np.log2(target_f0[target_f0 > 0]) assert np.allclose( np.log2(cents[1:-1]), np.log2(target_cents[1:-1]), rtol=0, atol=1e-2 ) # higher tolerance for the first and last frames, accounting for abrupt start / end assert np.abs(cents[0] - target_cents[0]) <= 1e-1 assert np.abs(cents[-1] - target_cents[-1]) <= 1e-1 @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize( "fmin,fmax,frame_length", [ (None, None, 2048), (110, None, 2048), (None, 880, 2048), ], ) def test_pyin_fail(fmin, fmax, frame_length): y = librosa.tone(110, duration=1.0) librosa.pyin(y, fmin=fmin, fmax=fmax, frame_length=frame_length) @pytest.mark.parametrize("freq", [110, 220, 440, 880]) @pytest.mark.parametrize("n_fft", [1024, 2048, 4096]) def test_piptrack(freq, n_fft): y = librosa.tone(freq, sr=22050, duration=0.2) S = np.abs(librosa.stft(y, n_fft=n_fft, center=False)) pitches, mags = librosa.piptrack(S=S, fmin=100) idx = mags > 0 assert len(idx) > 0 recovered_pitches = pitches[idx] # We should be within one cent of the target assert np.all(np.abs(np.log2(recovered_pitches) - np.log2(freq)) <= 1e-2) @pytest.mark.parametrize("n_fft", [1024, 2048]) def test_piptrack_magnitude_equivalence(n_fft): # piptrack rectifies its input to magnitudes internally. That rectification # is skipped for a real, non-negative spectrogram (to avoid a redundant # full-size copy), so a non-negative magnitude spectrogram, its complex # counterpart, and its sign-flipped version must all yield identical output. y = librosa.tone(440, sr=22050, duration=0.2) D = librosa.stft(y, n_fft=n_fft, center=False) S = np.abs(D) # Non-negative real input: the copy-skipping fast path. pitches_mag, mags_mag = librosa.piptrack(S=S, fmin=100) # Complex input: must be converted to magnitude. pitches_cplx, mags_cplx = librosa.piptrack(S=D, fmin=100) # Negative real input: must be rectified. pitches_neg, mags_neg = librosa.piptrack(S=-S, fmin=100) assert np.allclose(pitches_mag, pitches_cplx) assert np.allclose(mags_mag, mags_cplx) assert np.allclose(pitches_mag, pitches_neg) assert np.allclose(mags_mag, mags_neg) @pytest.mark.parametrize("center_note", [69, 84, 108]) @pytest.mark.parametrize("tuning", np.linspace(-0.5, 0.5, 5, endpoint=False)) @pytest.mark.parametrize("bins_per_octave", [12]) @pytest.mark.parametrize("resolution", [1e-2]) @pytest.mark.parametrize("sr", [11025, 22050]) def test_estimate_tuning( sr, center_note: int, tuning: float, bins_per_octave, resolution ): target_hz = librosa.midi_to_hz(center_note + tuning) y = librosa.tone(target_hz, duration=0.5, sr=sr) tuning_est = librosa.estimate_tuning( resolution=resolution, bins_per_octave=bins_per_octave, y=y, sr=sr, n_fft=2048, fmin=librosa.note_to_hz("C4"), fmax=librosa.note_to_hz("G#9"), ) # Round to the proper number of decimals deviation = np.around(tuning - tuning_est, int(-np.log10(resolution))) # Take the minimum floating point for positive and negative deviations max_dev = np.min([np.mod(deviation, 1.0), np.mod(-deviation, 1.0)]) # We'll accept an answer within three bins of the resolution assert max_dev <= 3 * resolution @pytest.mark.parametrize("y", [np.zeros(4000)]) @pytest.mark.parametrize("sr", [11025, 22050]) @pytest.mark.parametrize("resolution", [1e-2]) @pytest.mark.parametrize("bins_per_octave", [12]) def test_estimate_tuning_null(y, sr, resolution, bins_per_octave): with pytest.warns(UserWarning, match="Trying to estimate tuning"): tuning_est = librosa.estimate_tuning( resolution=resolution, bins_per_octave=bins_per_octave, y=y, sr=sr ) assert np.allclose(tuning_est, 0) @pytest.mark.parametrize("n_fft", [1024, 755, 2048, 2049]) @pytest.mark.parametrize("hop_length", [None, 512]) @pytest.mark.parametrize("power", [1, 2]) def test__spectrogram(y_22050, n_fft, hop_length, power): y = y_22050 S = np.abs(librosa.stft(y, n_fft=n_fft, hop_length=hop_length)) ** power S_, n_fft_ = librosa.core.spectrum._spectrogram( y=y, S=S, n_fft=n_fft, hop_length=hop_length, power=power ) # First check with all parameters assert np.allclose(S, S_) assert np.allclose(n_fft, n_fft_) # Then check with only the audio S_, n_fft_ = librosa.core.spectrum._spectrogram( y=y, n_fft=n_fft, hop_length=hop_length, power=power ) assert np.allclose(S, S_) assert np.allclose(n_fft, n_fft_) # And only the spectrogram S_, n_fft_ = librosa.core.spectrum._spectrogram( S=S, n_fft=n_fft, hop_length=hop_length, power=power ) assert np.allclose(S, S_) assert np.allclose(n_fft, n_fft_) # And only the spectrogram with no shape parameters S_, n_fft_ = librosa.core.spectrum._spectrogram(S=S, power=power) assert np.allclose(S, S_) if n_fft % 2 == 0: # Inference will be wrong if the frame length was odd assert np.allclose(n_fft, n_fft_) # And only the spectrogram but with incorrect n_fft S_, n_fft_ = librosa.core.spectrum._spectrogram(S=S, n_fft=2 * n_fft, power=power) assert np.allclose(S, S_) assert np.allclose(2 * (S.shape[-2] - 1), n_fft_) @pytest.mark.xfail(raises=librosa.ParameterError) def test__spectrogram_no_nfft(): librosa.core.spectrum._spectrogram(S=None, n_fft=None) @pytest.mark.parametrize( "x", [np.linspace(0, 2e5, num=10), np.linspace(0, 2e5, num=10) * np.exp(1.0j)] ) @pytest.mark.parametrize("ref", [1.0, np.max]) @pytest.mark.parametrize("amin", [1e-10, 1e3]) @pytest.mark.parametrize("top_db", [None, 0, 40, 80]) def test_power_to_db(x, ref, amin, top_db): if np.iscomplexobj(x): with pytest.warns(UserWarning, match="power_to_db was called on complex input"): y = librosa.power_to_db(x, ref=ref, amin=amin, top_db=top_db) else: y = librosa.power_to_db(x, ref=ref, amin=amin, top_db=top_db) assert np.isrealobj(y) assert y.shape == x.shape if top_db is not None: assert y.min() >= y.max() - top_db @pytest.mark.parametrize("x", [np.ones(10)]) @pytest.mark.parametrize("top_db,amin", [(20, -1), (20, 0), (-5, 1e-10)]) @pytest.mark.xfail(raises=librosa.ParameterError) def test_power_to_db_fail(x, top_db, amin): librosa.power_to_db(x, amin=amin, top_db=top_db) @pytest.mark.parametrize("erp", range(-5, 6)) @pytest.mark.parametrize("k", range(-5, 6)) def test_power_to_db_inv(erp, k): y_true = (k - erp) * 10 x = 10.0**k rp = 10.0**erp y = librosa.power_to_db(x, ref=rp, top_db=None) assert np.isclose(y, y_true) def test_amplitude_to_db(rng): NOISE_FLOOR = 1e-6 # Make some noise x = np.abs(rng.standard_normal(size=1000)) + NOISE_FLOOR db1 = librosa.amplitude_to_db(x, top_db=None) db2 = librosa.power_to_db(x**2, top_db=None) assert np.allclose(db1, db2) def test_amplitude_to_db_complex(rng): NOISE_FLOOR = 1e-6 # Make some noise x = np.abs(rng.standard_normal(size=1000)) + NOISE_FLOOR with pytest.warns(UserWarning, match="amplitude_to_db was called on complex input"): db1 = librosa.amplitude_to_db(x.astype(complex), top_db=None) db2 = librosa.power_to_db(x**2, top_db=None) assert np.allclose(db1, db2) def test_amplitude_to_db_scalar(): assert np.isclose(librosa.amplitude_to_db(1), 0) assert np.isclose(librosa.amplitude_to_db(2), 6.0206) def test_power_to_db_scalar(): assert np.isclose(librosa.power_to_db(1), 0) assert np.isclose(librosa.power_to_db(2), 3.0103) def test_power_to_db_bad_reducer(): x = np.ones((10,10)) def mymax(z, axis=None): # A bad reducer that does not support keepdims return np.max(z, axis=axis) with pytest.raises(librosa.ParameterError): librosa.power_to_db(x, ref=mymax) def test_amplitude_to_db_bad_reducer(): x = np.ones((10,10)) def mymax(z, axis=None): # A bad reducer that does not support keepdims return np.max(z, axis=axis) with pytest.raises(librosa.ParameterError): librosa.amplitude_to_db(x, ref=mymax) def test_db_to_amplitude_scalar(): assert np.isclose(librosa.db_to_amplitude(0), 1) assert np.isclose(librosa.db_to_amplitude(6.0206), 2) def test_db_to_power_scalar(): assert np.isclose(librosa.db_to_power(0), 1) assert np.isclose(librosa.db_to_power(3.0103), 2) @pytest.mark.parametrize("ref_p", range(-3, 4)) @pytest.mark.parametrize("xp", [np.arange(1e-5, 500, 0.5) ** 2]) def test_db_to_power_inv(ref_p, xp): ref = 10.0**ref_p db = librosa.power_to_db(xp, ref=ref, top_db=None) xp2 = librosa.db_to_power(db, ref=ref) assert np.allclose(xp, xp2) @pytest.mark.parametrize("erp", range(-5, 6)) @pytest.mark.parametrize("db", range(-100, 101, 10)) def test_db_to_power(erp, db): y = db rp = 10.0**erp x_true = 10.0**erp * (10.0 ** (0.1 * db)) x = librosa.db_to_power(y, ref=rp) assert np.isclose(x, x_true), (x, x_true, y, rp) @pytest.mark.parametrize("ref_p", range(-3, 4)) @pytest.mark.parametrize("xp", [np.arange(1e-5, 500, 0.5)]) def test_db_to_amplitude_inv(xp, ref_p): ref = 10.0**ref_p db = librosa.amplitude_to_db(xp, ref=ref, top_db=None) xp2 = librosa.db_to_amplitude(db, ref=ref) assert np.allclose(xp, xp2) def test_db_to_amplitude(rng): NOISE_FLOOR = 1e-6 # Make some noise x = np.abs(rng.standard_normal(size=1000)) + NOISE_FLOOR db = librosa.amplitude_to_db(x, top_db=None) x2 = librosa.db_to_amplitude(db) assert np.allclose(x, x2) @pytest.mark.parametrize("times", [np.linspace(0, 10.0, num=5)]) @pytest.mark.parametrize("sr", [11025, 22050]) @pytest.mark.parametrize("hop_length", [512, 1024]) @pytest.mark.parametrize("click", [None, np.ones(1000)]) @pytest.mark.parametrize("use_frames", [False, True]) @pytest.mark.parametrize("click_freq", [1000]) @pytest.mark.parametrize("click_duration", [0.1]) @pytest.mark.parametrize("length", [None, 5 * 22050]) def test_clicks( times, sr, hop_length, click_freq, click_duration, click, length, use_frames ): if use_frames: frames = librosa.time_to_frames(times, sr=sr, hop_length=hop_length) times = None else: frames = None y = librosa.clicks( times=times, frames=frames, sr=sr, hop_length=hop_length, click_freq=click_freq, click_duration=click_duration, click=click, length=length, ) if times is not None: nmax = librosa.time_to_samples(times, sr=sr).max() else: assert frames is not None nmax = librosa.frames_to_samples(frames, hop_length=hop_length).max() if length is not None: assert len(y) == length elif click is not None: assert len(y) == nmax + len(click) @pytest.mark.parametrize( "times,click_freq,click_duration,click,length", [ (None, 1000, 0.1, None, None), ([0, 2, 4, 8], 1000, 0.1, None, 0), ([0, 2, 4, 8], 0, 0.1, None, None), ([0, 2, 4, 8], 1000, 0, None, None), ], ) @pytest.mark.xfail(raises=librosa.ParameterError) def test_clicks_fail(times, click_freq, click_duration, click, length): librosa.clicks( times=times, frames=None, sr=22050, hop_length=512, click_freq=click_freq, click_duration=click_duration, click=click, length=length, ) @pytest.mark.parametrize("frequency", [440]) @pytest.mark.parametrize("sr", [11025, 22050, 44100]) @pytest.mark.parametrize( "length,duration", [(None, 0.5), (1740, None), (22050, None), (1740, 0.5)] ) @pytest.mark.parametrize("phi", [None, np.pi]) def test_tone(frequency, sr, length, duration, phi): y = librosa.tone( frequency=frequency, sr=sr, length=length, duration=duration, phi=phi ) if length is not None: assert len(y) == length else: assert len(y) == int(duration * sr) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize( "frequency,length,duration", [(None, 22050, 1), (440, None, None)] ) def test_tone_fail(frequency, length, duration): librosa.tone(frequency=frequency, sr=22050, length=length, duration=duration) @pytest.mark.parametrize("fmin,fmax", [(440, 880), (880, 440)]) @pytest.mark.parametrize("sr", [11025, 22050]) @pytest.mark.parametrize("length,duration", [(None, 0.5), (11025, None), (11025, 0.5)]) @pytest.mark.parametrize("phi", [None, np.pi / 2]) @pytest.mark.parametrize("linear", [False, True]) def test_chirp(fmin, fmax, sr, length, duration, linear, phi): y = librosa.chirp( fmin=fmin, fmax=fmax, sr=sr, length=length, duration=duration, linear=linear, phi=phi, ) if length is not None: assert len(y) == length else: assert len(y) == int(duration * sr) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize( "fmin,fmax,length,duration", [ (None, None, 22050, 1), (440, None, 22050, 1), (None, 880, 22050, 1), (440, 880, None, None), ], ) def test_chirp_fail(fmin, fmax, length, duration): librosa.chirp(fmin=fmin, fmax=fmax, sr=22050, length=length, duration=duration) @pytest.fixture(scope="module", params=[1, 2, 3]) def OVER_SAMPLE(request): return request.param @pytest.fixture(scope="module", params=[1, 2, 3.0 / 2, 5.0 / 4, 9.0 / 8]) def SCALE(request): return request.param @pytest.fixture(scope="module") def y_res(SCALE): y = np.sin(2 * np.pi * np.linspace(0, 1, num=int(SCALE * 256), endpoint=False)) y /= np.sqrt(SCALE) return y @pytest.fixture(scope="module") def y_orig(): return np.sin(2 * np.pi * np.linspace(0, 1, num=256, endpoint=False)) @pytest.mark.parametrize( "kind,atol", [("slinear", 1e-4), ("quadratic", 1e-5), ("cubic", 1e-6)] ) @pytest.mark.parametrize("n_fmt", [None, 64, 128, 256, 512]) def test_fmt_scale(y_orig, y_res, n_fmt, kind, atol, SCALE, OVER_SAMPLE): # Make sure our signals preserve energy assert np.allclose(np.sum(y_orig**2), np.sum(y_res**2)) # Scale-transform the original f_orig = librosa.fmt( y_orig, t_min=0.5, n_fmt=n_fmt, over_sample=OVER_SAMPLE, kind=kind ) # Force to the same length n_fmt_res = 2 * len(f_orig) - 2 # Scale-transform the new signal to match f_res = librosa.fmt( y_res, t_min=SCALE * 0.5, n_fmt=n_fmt_res, over_sample=OVER_SAMPLE, kind=kind ) # Due to sampling alignment, we'll get some phase deviation here # The shape of the spectrum should be approximately preserved though. assert np.allclose(np.abs(f_orig), np.abs(f_res), atol=atol, rtol=1e-7) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("y", [np.ones(128)]) @pytest.mark.parametrize( "t_min,n_fmt,over_sample", [ (-1, None, 2), (0, None, 2), (1, -1, 2), (1, 0, 2), (1, 1, 2), (1, 2, 2), (1, None, -1), (1, None, 0), (1, None, 0.5), ], ) def test_fmt_fail(y, t_min, n_fmt, over_sample): librosa.fmt(y, t_min=t_min, n_fmt=n_fmt, over_sample=over_sample) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("y", [np.inf + np.ones(128), np.ones(2)], ids=["inf", "short"]) def test_fmt_fail_badinput(y): librosa.fmt(y, t_min=1, n_fmt=128, over_sample=1) def test_fmt_axis(rng): y = rng.standard_normal(size=(32, 32)) f1 = librosa.fmt(y, axis=-1) f2 = librosa.fmt(y.T, axis=0).T assert np.allclose(f1, f2) def test_harmonics_1d(): x = np.arange(16) y = np.linspace(-8, 8, num=len(x), endpoint=False) ** 2 h = [0.25, 0.5, 1, 2, 4] yh = librosa.interp_harmonics(y, freqs=x, harmonics=h, axis=0) assert yh.shape[1:] == y.shape assert yh.shape[0] == len(h) for i in range(len(h)): if h[i] <= 1: # Check that subharmonics match step = int(1.0 / h[i]) vals = yh[i, ::step] assert np.allclose(vals, y[: len(vals)]) else: # Else check that harmonics match step = cast(int, h[i]) vals = y[::step] assert np.allclose(vals, yh[i, : len(vals)]) def test_harmonics_2d(): x = np.arange(16) y = np.linspace(-8, 8, num=len(x), endpoint=False) ** 2 y = np.tile(y, (5, 1)).T h = [0.25, 0.5, 1, 2, 4] yh = librosa.interp_harmonics(y, freqs=x, harmonics=h, axis=0) assert yh.shape[1:] == y.shape assert yh.shape[0] == len(h) for i in range(len(h)): if h[i] <= 1: # Check that subharmonics match step = int(1.0 / h[i]) vals = yh[i, ::step] assert np.allclose(vals, y[: len(vals)]) else: # Else check that harmonics match step = cast(int, h[i]) vals = y[::step] assert np.allclose(vals, yh[i, : len(vals)]) def test_harmonics_1d_nonunique(): x = np.arange(-8, 8) ** 2 y = np.linspace(-8, 8, num=len(x), endpoint=False) ** 2 h = [0.25, 0.5, 1, 2, 4] with pytest.warns(UserWarning): yh = librosa.interp_harmonics(y, freqs=x, harmonics=h, axis=0) @pytest.mark.xfail(raises=librosa.ParameterError) def test_harmonics_badshape_1d(): freqs = np.zeros(100) obs = np.zeros((5, 10)) librosa.interp_harmonics(obs, freqs=freqs, harmonics=[1]) @pytest.mark.xfail(raises=librosa.ParameterError) def test_harmonics_badshape_2d(): freqs = np.zeros((5, 5)) obs = np.zeros((5, 10)) librosa.interp_harmonics(obs, freqs=freqs, harmonics=[1]) def test_harmonics_2d_varying(): x = np.arange(16) y = np.linspace(-8, 8, num=len(x), endpoint=False) ** 2 x = np.tile(x, (5, 1)).T y = np.tile(y, (5, 1)).T h = [0.25, 0.5, 1, 2, 4] yh = librosa.interp_harmonics(y, freqs=x, harmonics=h, axis=-2) assert yh.shape[1:] == y.shape assert yh.shape[0] == len(h) for i in range(len(h)): if h[i] <= 1: # Check that subharmonics match step = int(1.0 / h[i]) vals = yh[i, ::step] assert np.allclose(vals, y[: len(vals)]) else: # Else check that harmonics match step = cast(int, h[i]) vals = y[::step] assert np.allclose(vals, yh[i, : len(vals)]) def test_harmonics_2d_varying_nonunique(): x = np.arange(-8, 8) ** 2 y = np.linspace(-8, 8, num=len(x), endpoint=False) ** 2 x = np.tile(x, (5, 1)).T y = np.tile(y, (5, 1)).T h = [0.25, 0.5, 1, 2, 4] with pytest.warns(UserWarning): yh = librosa.interp_harmonics(y, freqs=x, harmonics=h, axis=-2) def test_show_versions(): # Nothing to test here, except that everything passes. librosa.show_versions() @pytest.mark.parametrize("flayout", ["ba", "sos"]) @pytest.mark.parametrize("center", [True, False]) @pytest.mark.parametrize("hop_length", [None, 1024]) def test_iirt(y_22050, flayout, center, hop_length): sr = 22050 T = librosa.iirt( y_22050, sr=sr, hop_length=hop_length, flayout=flayout, center=center ) if hop_length is None: real_hop_length = 512 else: real_hop_length = hop_length # Verify the shape assert T.shape[0] == 85 # Expected number of frames without centering is (y.shape[-1] - win_length) // hop_length + 1 n_frames = (y_22050.shape[-1] - 2048) // real_hop_length + 1 if center: assert T.shape[1] == y_22050.shape[-1] // real_hop_length + 1 else: assert T.shape[1] == n_frames # Verify that it's all real and non-negative assert np.isrealobj(T) assert np.all(T >= 0) # TODO: further verify this one? I guess? @pytest.mark.xfail(raises=librosa.ParameterError) def test_iirt_flayout1(y_22050): sr = 22050 librosa.iirt(y_22050, hop_length=1102, win_length=2205, flayout="foo") # type: ignore[arg-type] def test_iirt_peaks(): # Test for PR #1157 Fs = 4000 length = 180 click_times = [10, 50, 90, 130, 170] x = np.zeros(length * Fs) for click in click_times: x[click * Fs] = 1 win_length = 200 hop_length = 50 center_freqs = librosa.midi_to_hz(np.arange(40, 95)) sample_rates = np.asarray( len(np.arange(40, 46)) * [ 1000.0, ] + len(np.arange(46, 80)) * [ 1750.0, ] + len(np.arange(80, 95)) * [ 4000.0, ] ) X = librosa.iirt( x, center_freqs=center_freqs, sample_rates=sample_rates, win_length=win_length, hop_length=hop_length, ) for cur_band in X: cur_peaks = scipy.signal.find_peaks( cur_band, height=np.mean(cur_band), distance=1000 )[0] assert len(cur_peaks) == 5 cur_peak_times = cur_peaks * hop_length / Fs assert all(abs(cur_peak_times - click_times) < (2 * win_length / Fs)) def test_iirt_padding(): Fs = 22050 x = np.zeros(Fs) H = 512 num_frames = np.empty(5) for i in range(1, 6): N = i * H X = librosa.iirt(x, sr=Fs, hop_length=H, win_length=N, center=True) num_frames[i - 1] = X.shape[1] assert np.all(num_frames == num_frames[0]) @pytest.fixture(scope="module") def S_pcen(rng_mod): return np.abs(rng_mod.standard_normal(size=(9, 30))) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize( "gain,bias,power,b,time_constant,eps,ms", [ (-1, 1, 1, 0.5, 0.5, 1e-6, 1), # gain < 0 (1, -1, 1, 0.5, 0.5, 1e-6, 1), # bias < 0 (1, 1, -0.1, 0.5, 0.5, 1e-6, 1), # power < 0 (1, 1, 1, -2, 0.5, 1e-6, 1), # b < 0 (1, 1, 1, 2, 0.5, 1e-6, 1), # b > 1 (1, 1, 1, 0.5, -2, 1e-6, 1), # time_constant <= 0 (1, 1, 1, 0.5, 0.5, 0, 1), # eps <= 0 (1, 1, 1, 0.5, 0.5, 1e-6, 1.5), (1, 1, 1, 0.5, 0.5, 1e-6, 0), # max_size not int, < 1 ], ) def test_pcen_failures(gain, bias, power, b, time_constant, eps, ms, S_pcen): librosa.pcen( S_pcen, gain=gain, bias=bias, power=power, time_constant=time_constant, eps=eps, b=b, max_size=ms, ) @pytest.mark.parametrize("p", [0.5, 1, 2]) def test_pcen_power(S_pcen, p): # when b=1, gain=0, bias=0, all filtering is disabled; # this test just checks that power calculations work as expected P = librosa.pcen( S_pcen, gain=0, bias=0, power=p, b=1, time_constant=0.5, eps=1e-6, max_size=1 ) assert np.allclose(P, S_pcen**p) def test_pcen_ones(S_pcen): # when gain=1, bias=0, power=1, b=1, eps=1e-20, we should get all ones P = librosa.pcen( S_pcen, gain=1, bias=0, power=1, b=1, time_constant=0.5, eps=1e-20, max_size=1 ) assert np.allclose(P, np.ones_like(S_pcen)) @pytest.mark.parametrize("power", [0, 1e-3]) @pytest.mark.parametrize("bias", [0, 1]) def test_pcen_drc(S_pcen, bias, power): P = librosa.pcen(S_pcen, gain=0.0, bias=bias, power=power, eps=1e-20) if power == 0: ref = np.expm1(P) else: if bias == 0: ref = np.exp(1.0 / power * np.log(P)) else: ref = np.expm1(1.0 / power * np.log1p(P)) assert np.allclose(S_pcen, ref) def test_pcen_complex(): S = np.ones((9, 30), dtype=complex) Pexp = np.ones((9, 30)) with warnings.catch_warnings(record=True) as out: P = librosa.pcen( S, gain=1, bias=0, power=1, time_constant=0.5, eps=1e-20, b=1, max_size=1 ) if np.issubdtype(S.dtype, np.complexfloating): assert len(out) > 0 assert "complex" in str(out[0].message).lower() assert P.shape == S.shape assert np.all(P >= 0) assert np.all(np.isfinite(P)) if Pexp is not None: assert np.allclose(P, Pexp) @pytest.mark.parametrize("max_size", [1, 3]) @pytest.mark.parametrize("Z", [np.zeros((9, 30))]) def test_pcen_zeros(max_size, Z): P = librosa.pcen( Z, gain=0.98, bias=2.0, power=0.5, b=None, time_constant=0.395, eps=1e-6, max_size=max_size, ) # PCEN should map zeros to zeros assert np.allclose(P, Z) def test_pcen_axes(rng): # Make a power spectrogram X = rng.standard_normal(size=(3, 100, 50)) ** 2 # First, test that axis setting works P1 = librosa.pcen(X[0]) P1a = librosa.pcen(X[0], axis=-1) P2 = librosa.pcen(X[0].T, axis=0).T assert np.allclose(P1, P2) assert np.allclose(P1, P1a) # Test that it works with max-filtering P1 = librosa.pcen(X[0], max_size=3) P1a = librosa.pcen(X[0], axis=-1, max_size=3) P2 = librosa.pcen(X[0].T, axis=0, max_size=3).T assert np.allclose(P1, P2) assert np.allclose(P1, P1a) # Test that it works with multi-dimensional input, no filtering P0 = librosa.pcen(X[0]) P1 = librosa.pcen(X[1]) P2 = librosa.pcen(X[2]) Pa = librosa.pcen(X) assert np.allclose(P0, Pa[0]) assert np.allclose(P1, Pa[1]) assert np.allclose(P2, Pa[2]) # Test that it works with multi-dimensional input, max-filtering P0 = librosa.pcen(X[0], max_size=3) P1 = librosa.pcen(X[1], max_size=3) P2 = librosa.pcen(X[2], max_size=3) Pa = librosa.pcen(X, max_size=3, max_axis=1) assert np.allclose(P0, Pa[0]) assert np.allclose(P1, Pa[1]) assert np.allclose(P2, Pa[2]) @pytest.mark.xfail(raises=librosa.ParameterError) def test_pcen_axes_nomax(rng): # Make a power spectrogram X = rng.standard_normal(size=(3, 100, 50)) ** 2 librosa.pcen(X, max_size=3) @pytest.mark.xfail(raises=librosa.ParameterError) def test_pcen_max1(): librosa.pcen(np.arange(100), max_size=3) def test_pcen_ref(rng): # Make a power spectrogram X = rng.standard_normal(size=(100, 50)) ** 2 # Edge cases: # gain=1, bias=0, power=1, b=1 => ones ones = np.ones_like(X) Y = librosa.pcen(X, gain=1, bias=0, power=1, b=1, eps=1e-20) assert np.allclose(Y, ones) # with ref=ones, we should get X / (eps + ones) == X Y2 = librosa.pcen(X, gain=1, bias=0, power=1, b=1, ref=ones, eps=1e-20) assert np.allclose(Y2, X) @pytest.mark.parametrize("x", [np.arange(100), np.arange(100).reshape((10, 10))]) def test_pcen_stream(x): if x.ndim == 1: x1 = x[:20] x2 = x[20:] else: x1 = x[:, :20] x2 = x[:, 20:] p1, zf1 = librosa.pcen(x1, return_zf=True) p2, zf2 = librosa.pcen(x2, zi=zf1, return_zf=True) pfull = librosa.pcen(x) assert np.allclose(pfull, np.hstack([p1, p2])) @pytest.mark.parametrize("axis", [0, 1, 2, -2, -1]) def test_pcen_stream_multi(axis, rng): # Generate a random power spectrum x = rng.standard_normal(size=(20, 50, 60)) ** 2 # Make slices along the target axis slice1 = [slice(None)] * x.ndim slice1[axis] = slice(0, 10) slice2 = [slice(None)] * x.ndim slice2[axis] = slice(10, None) slice1 = tuple(slice1) slice2 = tuple(slice2) # Compute pcen piecewise p1, zf1 = librosa.pcen(x[slice1], return_zf=True, axis=axis) p2, zf2 = librosa.pcen(x[slice2], zi=zf1, return_zf=True, axis=axis) # And the full pcen pfull = librosa.pcen(x, axis=axis) # Compare full to concatenated results assert np.allclose(pfull, np.concatenate([p1, p2], axis=axis)) @pytest.fixture(scope="module") def y_chirp(): sr = 22050 y = librosa.chirp(fmin=55, fmax=55 * 2**7, length=sr // 8, sr=sr) return y @pytest.mark.parametrize("hop_length", [None, 1024]) @pytest.mark.parametrize("win_length", [None, 1024]) @pytest.mark.parametrize("n_fft", [2048, 2049]) @pytest.mark.parametrize("window", ["hann", "rect"]) @pytest.mark.parametrize("center", [False, True]) @pytest.mark.parametrize("use_length", [False, True]) @pytest.mark.parametrize("pad_mode", ["constant", "reflect"]) @pytest.mark.parametrize("init", [None, "random"]) def test_griffinlim( y_chirp, hop_length, win_length, n_fft, window, center, use_length, pad_mode, init, ): if use_length: length = len(y_chirp) else: length = None D = librosa.stft( y_chirp, hop_length=hop_length, win_length=win_length, n_fft=n_fft, window=window, center=center, pad_mode=pad_mode, ) S = np.abs(D) y_rec = librosa.griffinlim( S, hop_length=hop_length, win_length=win_length, n_fft=n_fft, window=window, center=center, length=length, pad_mode=pad_mode, n_iter=1, init=init, ) # check length if use_length: assert len(y_rec) == length # Verify that the reconstruction is real-valued assert np.isrealobj(y_rec) assert np.all(np.isfinite(y_rec)) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) def test_griffinlim_dtype(y_chirp, dtype): D = librosa.stft(y_chirp) y_rec = librosa.griffinlim(np.abs(D), dtype=dtype, n_iter=1) assert y_rec.dtype == dtype @pytest.mark.parametrize("momentum", [0, 0.99]) def test_griffinlim_momentum(y_chirp, momentum): D = librosa.stft(y_chirp) librosa.griffinlim(np.abs(D), momentum=momentum, n_iter=1) # No value tests here, just ensuring that it passes cleanly @pytest.mark.parametrize("rng", [None, 0, np.random.RandomState()]) def test_griffinlim_state(y_chirp, rng): D = librosa.stft(y_chirp) y_rec = librosa.griffinlim(np.abs(D), rng=rng, n_iter=1) y_rec2 = librosa.griffinlim(np.abs(D), rng=rng, n_iter=1) # Ensure that seeding with a constant gives us consistent values if not isinstance(rng, np.random.RandomState) and rng is not None: assert np.allclose(y_rec, y_rec2) @pytest.mark.xfail(raises=librosa.ParameterError) def test_griffinlim_badinit(): x = np.zeros((33, 3)) librosa.griffinlim(x, init="garbage") @pytest.mark.xfail(raises=TypeError) def test_griffinlim_badrng(): x = np.zeros((33, 3)) librosa.griffinlim(x, rng="garbage") # type: ignore @pytest.mark.xfail(raises=librosa.ParameterError) def test_griffinlim_oldrng(): x = np.zeros((33, 3)) # We can't have both parameters set librosa.griffinlim(x, rng=0, random_state=0) def test_griffinlim_deprecated_randomstate(y_chirp): # Test that the deprecated random_state argument raises a warning D = librosa.stft(y=y_chirp) with pytest.warns(FutureWarning, match="renamed to 'rng'"): y1 = librosa.griffinlim( np.abs(D), n_iter=2, random_state=5, ) # And verify that it produces the same output as the new rng argument y2 = librosa.griffinlim(np.abs(D), n_iter=2, rng=5) assert np.allclose(y1, y2) @pytest.mark.xfail(raises=librosa.ParameterError) def test_griffinlim_badmomentum(): x = np.zeros((33, 3)) librosa.griffinlim(x, momentum=-1) def test_griffinlim_momentum_warn(): x = np.zeros((33, 3)) with pytest.warns(UserWarning): librosa.griffinlim(x, momentum=2) @pytest.mark.skip(reason="This test is not valid anymore") @pytest.mark.parametrize("ext", ["wav", "mp3"]) def test_get_samplerate(ext): path = os.path.join("tests", "data", os.path.extsep.join(["test1_22050", ext])) sr = librosa.get_samplerate(path) assert sr == 22050 def test_get_samplerate_soundfile(): path = os.path.join("tests", "test_audio.ogg") sfo = soundfile.SoundFile(path) sr2 = librosa.get_samplerate(sfo) assert sr2 == 44100 @pytest.fixture(params=["as_file", "as_string", "as_sfo"]) def path(request): # test data is stereo, int 16 path = os.path.join("tests", "test_audio.ogg") if request.param == "as_string": yield path elif request.param == "as_file": with open(path, "rb") as f: yield f elif request.param == "as_sfo": with soundfile.SoundFile(path) as f: yield f @pytest.mark.parametrize( "block_length,frame_length,hop_length", [(0, 1024, 512), (10, 0, 512), (10, 1024, 0)], ) def test_stream_badparam(path, block_length, frame_length, hop_length): with pytest.raises(librosa.ParameterError): next( librosa.stream( path, block_length=block_length, frame_length=frame_length, hop_length=hop_length, sr=None, ) ) def test_stream_bad_sr(path): with pytest.raises(librosa.ParameterError): next(librosa.stream(path, block_length=10, frame_length=2048, hop_length=512, sr=-1)) def test_stream_bad_res_type(path): with pytest.raises(librosa.ParameterError): next(librosa.stream(path, block_length=10, frame_length=2048, hop_length=512, sr=16000, res_type="foo")) def test_stream_bad_hop(path): # Fail if our hop length would not be integer-valued at the native # sampling rate with pytest.raises(librosa.ParameterError): next(librosa.stream(path, block_length=3, frame_length=2048, hop_length=513, sr=16000)) def _verify_stream_parity( path, block_length=10, frame_length=1024, hop_length=512, mono=True, offset=0.0, duration=None, fill_value=None, dtype=np.float32, sr=None, res_type="soxr_hq" ): stream = librosa.stream( path, sr=sr, block_length=block_length, frame_length=frame_length, hop_length=hop_length, dtype=dtype, mono=mono, offset=offset, duration=duration, fill_value=fill_value, res_type=res_type ) y_frame_stream = [] target_length = frame_length + (block_length - 1) * hop_length for y_block in stream: assert y_block.dtype == dtype if mono: assert y_block.ndim == 1 else: assert y_block.ndim == 2 assert y_block.shape[0] == 2 if fill_value is None: assert y_block.shape[-1] <= target_length else: assert y_block.shape[-1] == target_length y_b_mono = librosa.to_mono(y_block) if len(y_b_mono) >= frame_length: y_b_frame = librosa.util.frame(y_b_mono, frame_length=frame_length, hop_length=hop_length) y_frame_stream.append(y_b_frame) y_frame_stream = np.concatenate(y_frame_stream, axis=1) if hasattr(path, "seek"): path.seek(0) y_full, _ = librosa.load( path, sr=sr, dtype=dtype, mono=True, offset=offset, duration=duration, res_type=res_type ) y_frame = librosa.util.frame(y_full, frame_length=frame_length, hop_length=hop_length) n = y_frame.shape[1] assert np.allclose(y_frame[:, :n], y_frame_stream[:, :n]) @pytest.mark.parametrize("block_length", [10, np.int64(30)]) @pytest.mark.parametrize("frame_length", [1024, np.int64(2048)]) @pytest.mark.parametrize("hop_length", [512, np.int64(1024)]) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) @pytest.mark.parametrize("mono", [False, True]) def test_stream_geometry_and_types(path, block_length, frame_length, hop_length, dtype, mono): _verify_stream_parity( path, block_length=block_length, frame_length=frame_length, hop_length=hop_length, dtype=dtype, mono=mono ) @pytest.mark.parametrize("sr", [None, 11025, 11025.0]) @pytest.mark.parametrize("res_type", ["soxr_qq", "soxr_hq"]) def test_stream_resampling(path, sr, res_type): _verify_stream_parity(path, sr=sr, res_type=res_type) @pytest.mark.parametrize("offset", [0.0, 2.0, -2.0]) @pytest.mark.parametrize("duration", [None, 1.0]) def test_stream_time_bounds(path, offset, duration): _verify_stream_parity(path, offset=offset, duration=duration) @pytest.mark.parametrize("fill_value", [0.0, 999.0]) def test_stream_padding(path, fill_value): _verify_stream_parity(path, fill_value=fill_value) @pytest.mark.parametrize("mu", [15, 31, 255]) @pytest.mark.parametrize("quantize", [False, True]) def test_mu_compress(mu, quantize): x = np.linspace(-1, 1, num=5, endpoint=True) y = librosa.mu_compress(x, mu=mu, quantize=quantize) # Do we preserve sign? assert np.all(np.sign(y) == np.sign(x)) if quantize: # Check that y is between -(mu+1) and mu assert np.all(y >= -(mu + 1)) and np.all(y <= mu) assert np.issubdtype(y.dtype, np.integer) else: assert np.all(y >= -1) and np.all(y <= 1) @pytest.mark.xfail(raises=librosa.ParameterError) def test_mu_compress_badmu(): x = np.linspace(-1, 1, num=5) librosa.mu_compress(x, mu=-1) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize( "x", [ np.linspace(-2, 1, num=5, endpoint=True), np.linspace(-1, 2, num=5, endpoint=True), ], ) def test_mu_compress_badx(x): librosa.mu_compress(x) @pytest.mark.parametrize("mu", [15, 31, 255]) @pytest.mark.parametrize("quantize", [False, True]) def test_mu_expand(mu, quantize): # Really this is an integration test for companding. YOLO x = np.linspace(-1, 1, num=5, endpoint=True) y = librosa.mu_compress(x, mu=mu, quantize=quantize) z = librosa.mu_expand(y, mu=mu, quantize=quantize) assert isinstance(z, np.ndarray) assert np.all(z <= 1) and np.all(z >= -1) assert np.all(np.sign(z) == np.sign(x)) if not quantize: # Without quantization, companding should be exact assert np.allclose(x, z) @pytest.mark.xfail(raises=librosa.ParameterError) def test_mu_expand_badmu(): x = np.linspace(-1, 1, num=5) librosa.mu_expand(x, mu=-1) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize( "x", [ np.linspace(-2, 1, num=5, endpoint=True), np.linspace(-1, 2, num=5, endpoint=True), ], ) def test_mu_expand_badx(x): librosa.mu_expand(x, quantize=False) @pytest.mark.xfail(raises=librosa.ParameterError) def test_stft_bad_prealloc_shape(): y = np.zeros(22050) # Output shape here is incorrect, and should trigger a failure S1 = librosa.stft( y, n_fft=512, hop_length=128, out=np.zeros((100, 10), dtype=complex) ) @pytest.mark.xfail(raises=librosa.ParameterError) def test_stft_bad_prealloc_dtype(): y = np.zeros(22050) D = librosa.stft(y) Dbad = np.zeros(D.shape, dtype=np.float32) librosa.stft(y, out=Dbad) @pytest.mark.xfail(raises=librosa.ParameterError) def test_istft_bad_prealloc_shape(): D = np.zeros((1025, 5), dtype=np.complex64) librosa.istft(D, out=np.zeros(100)) def test_f0_harmonics_static(): freqs = np.arange(8) data = np.multiply.outer(freqs, np.arange(2, 5)) # array([[ 0, 0, 0], # [ 2, 3, 4], # [ 4, 6, 8], # [ 6, 9, 12], # [ 8, 12, 16], # [10, 15, 20], # [12, 18, 24], # [14, 21, 28]]) f0 = np.array([1, 2, 0]) harmonics = [0.5, 1, 3] yh = librosa.f0_harmonics(data, f0=f0, freqs=freqs, harmonics=harmonics) assert yh.shape[0] == len(harmonics) assert yh.shape[1:] == data.shape[1:] # The 1 here comes from linear interpolation of the 0.5 subharmonic # All else are data[1,2,3] assert np.allclose(yh[:, 0], [1, 2, 6]) # Values here come from f0 = 2 assert np.allclose(yh[:, 1], [3, 6, 18]) # Last frame has f0 = 0, so all harmonics will evaluate to 0 assert np.allclose(yh[:, 2], [0, 0, 0]) def test_f0_harmonics_dynamic(): # Cook up a dynamic frequency grid freqs = np.add.outer(np.arange(8), np.arange(3)) # array([[0, 1, 2], # [1, 2, 3], # [2, 3, 4], # [3, 4, 5], # [4, 5, 6], # [5, 6, 7], # [6, 7, 8], # [7, 8, 9]]) # Broadcast multiply to make some measurements data = freqs * np.arange(2, 5) # array([[ 0, 3, 8], # [ 2, 6, 12], # [ 4, 9, 16], # [ 6, 12, 20], # [ 8, 15, 24], # [10, 18, 28], # [12, 21, 32], # [14, 24, 36]]) # f0 at each frame f0 = np.array([2, 4, 5]) # Harmonics harmonics = [0.5, 1, 2] # Interpolate yh = librosa.f0_harmonics(data, f0=f0, freqs=freqs, harmonics=harmonics) assert yh.shape[0] == len(harmonics) assert yh.shape[1:] == data.shape[1:] # f0 in frame 0 = 2, f0 * h => [1, 2, 4] # data[f0 * h] = [2, 4, 8] assert np.allclose(yh[:, 0], [2, 4, 8]) # f0 in frame 1 = 4, f0 * h => [2, 4, 8] # data[f0 * h] = [6, 12, 24] assert np.allclose(yh[:, 1], [6, 12, 24]) # f0 in frame 2 = 5, f0 * h = [2.5, 5, 10] # interpolation happens here # last frequency falls off the edge of the frequency grid, filled as 0 assert np.allclose(yh[:, 2], [10, 20, 0]) def test_f0_harmonics_1d_nonunique(): freqs = np.arange(-8, 8) ** 2 data = np.multiply.outer(freqs, np.arange(5)) h = [1, 2, 3] f0 = np.ones(data.shape[-1]) with pytest.warns(UserWarning): librosa.f0_harmonics(data, freqs=freqs, harmonics=h, f0=f0) def test_f0_harmonics_2d_nonunique(): freqs = np.add.outer(np.arange(-8, 8) ** 2, np.arange(5)) data = freqs * np.arange(5) h = [1, 2, 3] f0 = np.ones(data.shape[-1]) with pytest.warns(UserWarning): librosa.f0_harmonics(data, freqs=freqs, harmonics=h, f0=f0) @pytest.mark.xfail(raises=librosa.ParameterError) def test_f0_harmonics_incompat(): # Freq axis does not match data shape freqs = np.arange(5) data = np.zeros((6, 7)) f0 = np.arange(7) harmonics = np.arange(1, 3) librosa.f0_harmonics(data, freqs=freqs, harmonics=harmonics, f0=f0) @pytest.mark.parametrize("rate", [0.5, 1, 3]) def test_phase_vocoder_fixed_rate(rate, rng): # Make a random complex spectrogram D = rng.standard_normal(size=(257, 12)) + 1j * rng.standard_normal(size=(257, 12)) D_stretch = librosa.phase_vocoder(D, rate=rate) # Check that the output has the correct shape assert D_stretch.shape == (D.shape[0], int(np.ceil(D.shape[1] / rate))) # Check that D interpolates the values properly if rate >= 1: assert np.allclose(np.abs(D[:, ::rate]), np.abs(D_stretch)) else: assert np.allclose(np.abs(D), np.abs(D_stretch[:, :: int(1 / rate)])) def test_phase_vocoder_variable_rate(rng): # Make a random complex spectrogram D = rng.standard_normal(size=(257, 12)) + 1j * rng.standard_normal(size=(257, 12)) rate = 0.5 # We'll make constant rate vector, but it won't cover the whole input duration # This is enough to verify the precomputed t_out code path t_out = np.arange(0, D.shape[1] - 2, rate) D_stretch = librosa.phase_vocoder(D, t_out=t_out) assert D_stretch.shape == (D.shape[0], len(t_out)) assert np.allclose(np.abs(D[:, :-2]), np.abs(D_stretch[:, :: int(1 / rate)])) @pytest.mark.xfail(raises=librosa.ParameterError) def test_phase_vocoder_bad_inputs(): D = np.zeros((257, 12), dtype=np.complex64) # Can only specify one of rate or t_out librosa.phase_vocoder(D, rate=1.0, t_out=np.arange(6)) def test_phase_vocoder_hop_deprecated(): D = np.zeros((257, 12), dtype=np.complex64) with pytest.warns(FutureWarning, match="`hop_length` parameter is deprecated"): librosa.phase_vocoder(D, rate=1.0, hop_length=64) def test_phase_vocoder_nfft_deprecated(): D = np.zeros((257, 12), dtype=np.complex64) with pytest.warns(FutureWarning, match="`n_fft` parameter is deprecated"): librosa.phase_vocoder(D, rate=1.0, n_fft=64) @pytest.mark.xfail(raises=librosa.ParameterError) def test_phase_vocoder_bad_t_out(): D = np.zeros((257, 12), dtype=np.complex64) t_out = np.arange(14) librosa.phase_vocoder(D, t_out=t_out) def test_phase_vocoder_nonmonotone(): D = np.zeros((257, 12), dtype=np.complex64) t_out = np.array([1, 0, 1, 2, 3]) with pytest.warns(UserWarning, match="t_out is not monotonic"): D_stretch = librosa.phase_vocoder(D, t_out=t_out) @pytest.mark.parametrize("rate", [0, -1]) @pytest.mark.xfail(raises=librosa.ParameterError) def test_phase_vocoder_negative_rate(rate): D = np.zeros((3, 2), dtype=np.complex64) librosa.phase_vocoder(D, rate=rate) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1786462635.232227 librosa-1.0.0/tests/test_decompose.py0000644000175100017510000001602315236640653017356 0ustar00runnerrunner#!/usr/bin/env python # CREATED: 2013-10-06 22:31:29 by Dawen Liang # unit tests for librosa.decompose # Disable cache import os try: os.environ.pop("LIBROSA_CACHE_DIR") except KeyError: pass import numpy as np import scipy.sparse import librosa import sklearn.decomposition import pytest from typing import Union def test_default_decompose(): X = np.array([[1, 2, 3, 4, 5, 6], [1, 1, 1.2, 1, 0.8, 1]]) (W, H) = librosa.decompose.decompose(X, random_state=0) assert np.allclose(X, W.dot(H), rtol=1e-2, atol=1e-2) def test_given_decompose(): D = sklearn.decomposition.NMF(random_state=0) X = np.array([[1, 2, 3, 4, 5, 6], [1, 1, 1.2, 1, 0.8, 1]]) (W, H) = librosa.decompose.decompose(X, transformer=D) assert np.allclose(X, W.dot(H), rtol=1e-2, atol=1e-2) def test_decompose_fit(rng): D = sklearn.decomposition.NMF(random_state=0) X = np.array([[1, 2, 3, 4, 5, 6], [1, 1, 1.2, 1, 0.8, 1]]) # Do a first fit (W, H) = librosa.decompose.decompose(X, transformer=D, fit=True) # Make random data and decompose with the same basis X = np.asarray(rng.standard_normal(size=X.shape) ** 2) (W2, H2) = librosa.decompose.decompose(X, transformer=D, fit=False) # Make sure the basis hasn't changed assert np.allclose(W, W2) @pytest.mark.xfail(raises=librosa.ParameterError) def test_decompose_multi_sort(): librosa.decompose.decompose(np.zeros((3, 3, 3)), sort=True) @pytest.mark.filterwarnings("ignore:Maximum number of iterations") def test_decompose_multi(rng): X = rng.random(size=(2, 20, 100)) # Fit with multichannel data components, activations = librosa.decompose.decompose( X, n_components=20, random_state=0 ) # Reshape the data Xflat = np.vstack([X[0], X[1]]) c_flat, a_flat = librosa.decompose.decompose(Xflat, n_components=20, random_state=0) assert np.allclose(c_flat[: X.shape[1]], components[0]) assert np.allclose(c_flat[X.shape[1] :], components[1]) assert np.allclose(activations, a_flat) @pytest.mark.xfail(raises=librosa.ParameterError) def test_decompose_fit_false(): X = np.array([[1, 2, 3, 4, 5, 6], [1, 1, 1.2, 1, 0.8, 1]]) (W, H) = librosa.decompose.decompose(X, fit=False) def test_sorted_decompose(): X = np.array([[1, 2, 3, 4, 5, 6], [1, 1, 1.2, 1, 0.8, 1]]) (W, H) = librosa.decompose.decompose(X, sort=True, random_state=0) assert np.allclose(X, W.dot(H), rtol=1e-2, atol=1e-2) @pytest.fixture(scope="module") def y22050(): sr = 22050 y = librosa.chirp(fmin=55, fmax=880, sr=sr, duration=5.0) y += librosa.clicks( times=np.arange(0, 5, 0.5), sr=sr, length=len(y), click_duration=0.25 ) return y @pytest.fixture(scope="module") def D22050(y22050): return librosa.stft(y22050) @pytest.fixture(scope="module") def S22050(D22050): return np.abs(D22050) @pytest.mark.parametrize("window", [31, (5, 5)]) @pytest.mark.parametrize("power", [1, 2]) @pytest.mark.parametrize("mask", [False, True]) @pytest.mark.parametrize("margin", [1.0, 3.0, (1.0, 1.0), (9.0, 10.0)]) def test_real_hpss(S22050, window, power, mask, margin): H, P = librosa.decompose.hpss( S22050, kernel_size=window, power=power, mask=mask, margin=margin ) if margin == 1.0 or margin == (1.0, 1.0): if mask: assert np.allclose(H + P, np.ones_like(S22050)) else: assert np.allclose(H + P, S22050) else: if mask: assert np.all(H + P <= np.ones_like(S22050)) else: assert np.all(H + P <= S22050) @pytest.mark.xfail(raises=librosa.ParameterError) def test_hpss_margin_error(S22050): H, P = librosa.decompose.hpss(S22050, margin=0.9) def test_complex_hpss(D22050): H, P = librosa.decompose.hpss(D22050) assert np.allclose(H + P, D22050) def test_nn_filter_mean(rng): X = rng.standard_normal(size=(10, 100)) # Build a recurrence matrix, just for testing purposes rec = librosa.segment.recurrence_matrix(X) X_filtered = librosa.decompose.nn_filter(X) # Normalize the recurrence matrix so dotting computes an average rec = librosa.util.normalize(rec.astype(float), axis=0, norm=1) assert np.allclose(X_filtered, X.dot(rec)) def test_nn_filter_mean_rec(rng): X = rng.standard_normal(size=(10, 100)) # Build a recurrence matrix, just for testing purposes rec = librosa.segment.recurrence_matrix(X) # Knock out the first three rows of links rec[:, :3] = False X_filtered = librosa.decompose.nn_filter(X, rec=rec) for i in range(3): assert np.allclose(X_filtered[:, i], X[:, i]) # Normalize the recurrence matrix rec = librosa.util.normalize(rec.astype(float), axis=0, norm=1) assert np.allclose(X_filtered[:, 3:], (X.dot(rec))[:, 3:]) def test_nn_filter_mean_rec_sparse(rng): X = rng.standard_normal(size=(10, 100)) # Build a recurrence matrix, just for testing purposes rec = librosa.segment.recurrence_matrix(X, sparse=True) X_filtered = librosa.decompose.nn_filter(X, rec=rec) # Normalize the recurrence matrix rec = librosa.util.normalize(rec.toarray().astype(float), axis=0, norm=1) assert np.allclose(X_filtered, (X.dot(rec))) @pytest.fixture(scope="module") def s_multi(): y = librosa.chirp(fmin=55, fmax=880, duration=5.0) y = np.vstack([y, y[::-1]]) return np.abs(librosa.stft(y)) @pytest.mark.parametrize("useR,sparse", [(False, False), (True, False), (True, True)]) def test_nn_filter_multi(s_multi, useR, sparse): R = librosa.segment.recurrence_matrix(s_multi, mode="affinity", sparse=sparse) if useR: R_multi = R else: R_multi = None s_filt = librosa.decompose.nn_filter( s_multi, rec=R_multi, mode="affinity", sparse=sparse ) # Always use the same recurrence matrix for comparison s_filt0 = librosa.decompose.nn_filter(s_multi[0], rec=R) s_filt1 = librosa.decompose.nn_filter(s_multi[1], rec=R) assert np.allclose(s_filt[0], s_filt0) assert np.allclose(s_filt[1], s_filt1) assert not np.allclose(s_filt0, s_filt1) def test_nn_filter_avg(rng): X = rng.standard_normal(size=(10, 100)) # Build a recurrence matrix, just for testing purposes rec = librosa.segment.recurrence_matrix(X, mode="affinity") X_filtered = librosa.decompose.nn_filter(X, rec=rec, aggregate=np.average) # Normalize the recurrence matrix so dotting computes an average rec = librosa.util.normalize(rec, axis=0, norm=1) assert np.allclose(X_filtered, X.dot(rec)) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize( "x,y", [(10, 10), (100, 20), (20, 100), (100, 101), (101, 101)] ) @pytest.mark.parametrize("sparse", [False, True]) @pytest.mark.parametrize("data", [np.zeros((10, 100))]) def test_nn_filter_badselfsim(data, x, y, sparse, rng): # Build a recurrence matrix, just for testing purposes rec: Union[np.ndarray, scipy.sparse.csr_array] = rng.standard_normal(size=(x, y)) if sparse: rec = scipy.sparse.csr_array(rec) librosa.decompose.nn_filter(data, rec=rec) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1786462635.232227 librosa-1.0.0/tests/test_display.py0000644000175100017510000022245015236640653017050 0ustar00runnerrunner#!/usr/bin/env python # -*- encoding: utf-8 -*- # CREATED:2015-02-14 22:51:01 by Brian McFee """Unit tests for display module""" # Disable cache import os try: os.environ.pop("LIBROSA_CACHE_DIR") except KeyError: pass import gc import weakref from packaging import version import pytest import matplotlib import matplotlib.collections import matplotlib.pyplot as plt from matplotlib.testing.conftest import text_placeholders from matplotlib import colormaps as mcm import librosa import librosa.display import numpy as np from typing import Any, Dict STYLE = "default" # Workaround for old freetype builds with our image fixtures FT_VERSION = version.parse(matplotlib.ft2font.__freetype_version__) OLD_FT = not (FT_VERSION >= version.parse("2.10")) @pytest.fixture(scope="module") def audio(): __EXAMPLE_FILE = os.path.join("tests", "test_audio.ogg") # Force 64-bit here to avoid phase instabilities in display down the road y, sr = librosa.load(__EXAMPLE_FILE, dtype=np.float64) return y, sr @pytest.fixture(scope="module") def y(audio): return audio[0] @pytest.fixture(scope="module") def sr(audio): return audio[1] @pytest.fixture(scope="module") def S(y): return librosa.stft(y) @pytest.fixture(scope="module") def S_abs(S): return np.abs(S) @pytest.fixture(scope="module") def C(y, sr): return np.abs(librosa.cqt(y, sr=sr)) @pytest.fixture(scope="module") def S_signed(S): return np.round(np.abs(S) - np.mean(np.abs(S)), decimals=4) @pytest.fixture(scope="module") def rhythm(y, sr): return librosa.beat.beat_track(y=y, sr=sr) @pytest.fixture(scope="module") def tempo(rhythm): return rhythm[0] @pytest.fixture(scope="module") def beats(rhythm, C): return librosa.util.fix_frames(rhythm[1]) @pytest.fixture(scope="module") def beat_t(beats, sr): return librosa.frames_to_time(beats, sr=sr) @pytest.fixture(scope="module") def Csync(C, beats): return librosa.util.sync(C, beats, aggregate=np.median) @pytest.mark.xfail(raises=librosa.ParameterError) def test_unknown_time_unit(y): times = np.arange(len(y)) plt.figure() ax = plt.gca() ax.plot(times, y) ax.xaxis.set_major_formatter( librosa.display.TimeFormatter(unit="an unsupported time unit") ) return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["complex"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) @pytest.mark.usefixtures('text_placeholders') def test_complex_input(S): plt.figure() with pytest.warns(UserWarning, match="Trying to display complex"): librosa.display.specshow(S) return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["abs"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) @pytest.mark.usefixtures('text_placeholders') def test_abs_input(S_abs): plt.figure() librosa.display.specshow(S_abs) return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["cqt_note"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_cqt_note(C): plt.figure() librosa.display.specshow(C, y_axis="cqt_note") return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["fft_note"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_fft_note(S_abs): plt.figure() librosa.display.specshow(S_abs, y_axis="fft_note") return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["cqt_hz"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_cqt_hz(C): plt.figure() librosa.display.specshow(C, y_axis="cqt_hz") return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["tempo"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_tempo(y, sr): T = librosa.feature.tempogram(y=y, sr=sr, win_length=64) plt.figure() librosa.display.specshow(T, y_axis="tempo", cmap="magma") return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["fourier_tempo"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) @pytest.mark.filterwarnings( "ignore:n_fft=.*is too large" ) # our test signal is short, but this is fine here def test_fourier_tempo(y, sr): T = librosa.feature.fourier_tempogram(y=y, sr=sr, win_length=64) plt.figure() librosa.display.specshow(np.abs(T), y_axis="fourier_tempo", cmap="magma") return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["tonnetz"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_tonnetz(C): plt.figure() chroma = librosa.feature.chroma_cqt(C=C) ton = librosa.feature.tonnetz(chroma=chroma) librosa.display.specshow(ton, y_axis="tonnetz") return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["chroma"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_chroma(S_abs, sr): plt.figure() plt.subplot(3, 1, 1) chr1 = librosa.feature.chroma_stft(S=S_abs**2, sr=sr) librosa.display.specshow(chr1, y_axis="chroma") plt.subplot(3, 1, 2) chr2 = librosa.feature.chroma_stft(S=S_abs**2, sr=sr, n_chroma=2 * 12) librosa.display.specshow(chr2, y_axis="chroma", bins_per_octave=2 * 12) plt.subplot(3, 1, 3) chr3 = librosa.feature.chroma_stft(S=S_abs**2, sr=sr, n_chroma=3 * 12) librosa.display.specshow(chr3, y_axis="chroma", bins_per_octave=3 * 12) return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["chroma_svara"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_chroma_svara(C, sr): chroma = librosa.feature.chroma_cqt(C=C, sr=sr, threshold=0.9) fig, (ax1, ax2, ax3, ax4) = plt.subplots(nrows=4, sharex=True, figsize=(10, 10)) # Hindustani, no thaat librosa.display.specshow(chroma, y_axis="chroma_h", Sa=5, ax=ax1) # Hindustani, kafi thaat librosa.display.specshow(chroma, y_axis="chroma_h", Sa=5, ax=ax2, thaat="kafi") # Carnatic, mela 22 librosa.display.specshow(chroma, y_axis="chroma_c", Sa=5, ax=ax3, mela=22) # Carnatic, mela 1 librosa.display.specshow(chroma, y_axis="chroma_c", Sa=7, ax=ax4, mela=1) ax1.label_outer() ax2.label_outer() ax3.label_outer() return fig @pytest.mark.mpl_image_compare( baseline_images=["double_chroma"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_double_chroma(S_abs, sr): plt.figure() chr1 = librosa.feature.chroma_stft(S=S_abs**2, sr=sr) chr1 = np.vstack((chr1, chr1)) librosa.display.specshow(chr1, y_axis="chroma", bins_per_octave=12) return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["x_mel"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_x_mel(S_abs): plt.figure() M = librosa.feature.melspectrogram(S=S_abs**2) librosa.display.specshow(M.T, x_axis="mel") return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["y_mel"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_y_mel(S_abs): plt.figure() M = librosa.feature.melspectrogram(S=S_abs**2) librosa.display.specshow(M, y_axis="mel") return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["y_mel_bounded"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_y_mel_bounded(S_abs): plt.figure() fmin, fmax = 110, 880 M = librosa.feature.melspectrogram(S=S_abs**2, fmin=fmin, fmax=fmax) librosa.display.specshow(M, y_axis="mel", fmin=fmin, fmax=fmax) return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["x_none_y_linear"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_xaxis_none_yaxis_linear(S_abs, S_signed): plt.figure() plt.subplot(2, 1, 1) librosa.display.specshow(S_abs, y_axis="linear") plt.subplot(2, 1, 2) librosa.display.specshow(S_signed, y_axis="fft") return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["specshow_ext_axes"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) @pytest.mark.usefixtures('text_placeholders') def test_specshow_ext_axes(S_abs): plt.figure() ax_left = plt.subplot(1, 2, 1) ax_right = plt.subplot(1, 2, 2) # implicitly ax_right librosa.display.specshow(S_abs, cmap="gray") librosa.display.specshow(S_abs, cmap="magma", ax=ax_left) return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["x_none_y_log"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_xaxis_none_yaxis_log(S_abs, S_signed): plt.figure() plt.subplot(2, 1, 1) librosa.display.specshow(S_abs, y_axis="log") plt.subplot(2, 1, 2) librosa.display.specshow(S_signed, y_axis="log") return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["x_linear_y_none"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_xaxis_linear_yaxis_none(S_abs, S_signed): plt.figure() plt.subplot(2, 1, 1) librosa.display.specshow(S_abs.T, x_axis="linear") plt.subplot(2, 1, 2) librosa.display.specshow(S_signed.T, x_axis="fft") return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["x_log_y_none"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_xaxis_log_yaxis_none(S_abs, S_signed): plt.figure() plt.subplot(2, 1, 1) librosa.display.specshow(S_abs.T, x_axis="log") plt.subplot(2, 1, 2) librosa.display.specshow(S_signed.T, x_axis="log") return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["x_time_y_none"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_xaxis_time_yaxis_none(S_abs): plt.figure() librosa.display.specshow(S_abs, x_axis="time") return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["x_none_y_time"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_xaxis_none_yaxis_time(S_abs): plt.figure() librosa.display.specshow(S_abs.T, y_axis="time") return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["x_frames_y_none"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_xaxis_frames_yaxis_none(S_abs): plt.figure() librosa.display.specshow(S_abs, x_axis="frames") return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["x_none_y_frames"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_xaxis_none_yaxis_frames(S_abs): plt.figure() librosa.display.specshow(S_abs.T, y_axis="frames") return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["x_lag_y_none"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_xaxis_lag_yaxis_none(S_abs): plt.figure() librosa.display.specshow(S_abs, x_axis="lag") return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["x_none_y_lag"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_xaxis_time_yaxis_lag(S_abs): plt.figure() librosa.display.specshow(S_abs.T, y_axis="lag") return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["time_scales_auto"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_time_scales_auto(S_abs, sr): # sr = 22050, hop_length = 512, S.shape[1] = 198 # 197 * 512 / 22050 ~= 4.6s plt.figure(figsize=(10, 10)) plt.subplot(4, 1, 1) # sr * 10 -> ms librosa.display.specshow(S_abs, sr=10 * sr, x_axis="time") plt.subplot(4, 1, 2) # sr -> s librosa.display.specshow(S_abs, sr=sr, x_axis="time") plt.subplot(4, 1, 3) # sr / 20 -> m librosa.display.specshow(S_abs, sr=sr // 20, x_axis="time") plt.subplot(4, 1, 4) # sr / (60 * 20) -> h librosa.display.specshow(S_abs, sr=sr // (60 * 20), x_axis="time") plt.tight_layout() return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["time_unit"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_time_unit(S_abs, sr): # sr = 22050, hop_length = 512, S.shape[1] = 198 # 197 * 512 / 22050 ~= 4.6s plt.figure(figsize=(9, 10)) plt.subplot(5, 1, 1) # time scale auto librosa.display.specshow(S_abs, sr=sr, x_axis="time") plt.subplot(5, 1, 2) # time unit fixed to 'h' librosa.display.specshow(S_abs, sr=sr, x_axis="h") plt.subplot(5, 1, 3) # time unit fixed to 'h' librosa.display.specshow(S_abs, sr=sr, x_axis="m") plt.subplot(5, 1, 4) # time unit fixed to 's' librosa.display.specshow(S_abs, sr=sr, x_axis="s") plt.subplot(5, 1, 5) # time unit fixed to 'ms' librosa.display.specshow(S_abs, sr=sr, x_axis="ms") plt.tight_layout() return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["time_unit_lag"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_time_unit_lag(S_abs, sr): plt.figure(figsize=(9, 10)) plt.subplot(5, 1, 1) # time scale auto in lag mode librosa.display.specshow(S_abs, sr=sr, x_axis="lag") plt.subplot(5, 1, 2) # time unit fixed to 'h' in lag mode librosa.display.specshow(S_abs, sr=sr, x_axis="lag_h") plt.subplot(5, 1, 3) # time unit fixed to 'm' in lag mode librosa.display.specshow(S_abs, sr=sr, x_axis="lag_m") plt.subplot(5, 1, 4) # time unit fixed to 's' in lag mode librosa.display.specshow(S_abs, sr=sr, x_axis="lag_s") plt.subplot(5, 1, 5) # time unit fixed to 'ms' in lag mode librosa.display.specshow(S_abs, sr=sr, x_axis="lag_ms") plt.tight_layout() return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["waveshow_mono"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) @pytest.mark.usefixtures('text_placeholders') def test_waveshow_mono(y, sr): fig, ax = plt.subplots() librosa.display.waveshow(y, sr=sr, ax=ax) return fig @pytest.mark.mpl_image_compare( baseline_images=["waveshow_mono_trans"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) @pytest.mark.usefixtures('text_placeholders') def test_waveshow_mono_trans(y, sr): fig, ax = plt.subplots() librosa.display.waveshow(y, sr=sr, ax=ax, transpose=True) return fig @pytest.mark.mpl_image_compare( baseline_images=["waveshow_mono_zoom"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) @pytest.mark.usefixtures('text_placeholders') def test_waveshow_mono_zoom(y, sr): fig, ax = plt.subplots() out = librosa.display.waveshow(y, sr=sr, ax=ax, max_points=sr // 2) # Zoom into 1/8 of a second, make sure it's out of the initial viewport ax.set(xlim=[1, 1.125]) return fig @pytest.mark.mpl_image_compare( baseline_images=["waveshow_mono_zoom_trans"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) @pytest.mark.usefixtures('text_placeholders') def test_waveshow_mono_zoom_trans(y, sr): fig, ax = plt.subplots() out = librosa.display.waveshow(y, sr=sr, ax=ax, max_points=sr // 2, transpose=True) # Zoom into 1/8 of a second, make sure it's out of the initial viewport ax.set(ylim=[1, 1.125]) return fig @pytest.mark.mpl_image_compare( baseline_images=["waveshow_mono_zoom_out"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) @pytest.mark.usefixtures('text_placeholders') def test_waveshow_mono_zoom_out(y, sr): fig, ax = plt.subplots() out = librosa.display.waveshow(y, sr=sr, ax=ax, max_points=sr // 2) # Zoom into 1/8 of a second, make sure it's out of the initial viewport ax.set(xlim=[1, 1.125]) # Zoom back out to get an envelope view again ax.set(xlim=[0, 1]) return fig @pytest.mark.mpl_image_compare( baseline_images=["waveshow_ext_axes"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) @pytest.mark.usefixtures('text_placeholders') def test_waveshow_ext_axes(y): plt.figure() ax_left = plt.subplot(1, 2, 1) ax_right = plt.subplot(1, 2, 2) # implicitly ax_right librosa.display.waveshow(y, color="blue") librosa.display.waveshow(y, color="red", ax=ax_left) return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["waveshow_inverted"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_waveshow_inverted(y, sr): fig, ax = plt.subplots(nrows=3, sharex=True) # Original waveshow librosa.display.waveshow(y, sr=sr, ax=ax[0], invert=False, label="Regular") ax[0].legend(loc="upper right") # Inverted with default (axes face) color librosa.display.waveshow( y, sr=sr, ax=ax[1], invert=True, invert_color=None, label="Inverted" ) ax[1].legend(loc="upper right") # Inverted with custom color librosa.display.waveshow( y, sr=sr, ax=ax[2], invert=True, invert_color="#2d2d2d", label="Inverted custom" ) ax[2].legend(loc="upper right") for axi in ax.flat: axi.label_outer() return fig @pytest.mark.mpl_image_compare( baseline_images=["waveshow_stereo"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) @pytest.mark.usefixtures('text_placeholders') def test_waveshow_stereo(y, sr): ys = librosa.util.stack([y, 2 * y]) plt.figure() librosa.display.waveshow(ys, sr=sr) return plt.gcf() @pytest.mark.xfail(raises=librosa.ParameterError) def test_unknown_wavaxis(y, sr): plt.figure() librosa.display.waveshow(y, sr=sr, axis="something not in the axis map") return plt.gcf() @pytest.mark.xfail(raises=librosa.ParameterError) def test_waveshow_unknown_wavaxis(y, sr): plt.figure() librosa.display.waveshow(y, sr=sr, axis="something not in the axis map") return plt.gcf() @pytest.mark.xfail(raises=librosa.ParameterError) def test_waveshow_bad_maxpoints(y, sr): plt.figure() librosa.display.waveshow(y, sr=sr, max_points=0) return plt.gcf() def test_waveshow_adaptor_survives_gc_single(y, sr): """Regression test for #1970: single waveshow adaptor should survive GC.""" fig, ax = plt.subplots() librosa.display.waveshow(y, sr=sr, ax=ax, max_points=sr // 2) gc.collect() ax.set_xlim(0, 0.025) fig.canvas.draw() # Data lines only (not ticks) visible_data_lines = [l for l in ax.lines if l.get_visible()] # Envelope(s) are PolyCollections in ax.collections polys = [ c for c in ax.collections if isinstance(c, matplotlib.collections.PolyCollection) ] assert ( len(visible_data_lines) >= 1 ), "Sample/step artist should be visible after zoom" assert all( not p.get_visible() for p in polys ), "Envelope should be hidden after zoom" plt.close(fig) def test_waveshow_adaptor_survives_gc_multi(y, sr): """Regression test for #1970: multiple waveshow adaptors should survive GC.""" fig, ax = plt.subplots() librosa.display.waveshow(y, sr=sr, ax=ax, color="blue", max_points=sr // 2) librosa.display.waveshow( y, sr=sr, ax=ax, color="red", alpha=0.5, zorder=-1, max_points=sr // 2 ) gc.collect() ax.set_xlim(0, 0.025) fig.canvas.draw() visible_data_lines = [l for l in ax.lines if l.get_visible()] polys = [ c for c in ax.collections if isinstance(c, matplotlib.collections.PolyCollection) ] assert ( len(visible_data_lines) >= 2 ), "Both sample/step artists should be visible after zoom" assert all( not p.get_visible() for p in polys ), "All envelopes should be hidden after zoom" plt.close(fig) def test_waveshow_registry_cleanup_on_axes_gc(): """Test that the adaptor registry uses WeakKeyDictionary correctly. This verifies the fix for #1970: adaptors are stored in a WeakKeyDictionary keyed by Axes, so they will be cleaned up when the Axes is garbage collected. """ from matplotlib.figure import Figure from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas fig = Figure() FigureCanvas(fig) ax = fig.add_subplot(111) ax_id = id(ax) y = np.sin(2 * np.pi * 440 * np.linspace(0, 1, 22050)) librosa.display.waveshow(y, sr=22050, ax=ax) # Our Axes should be in the registry before_keys = set(map(id, librosa.display._WAVESHOW_ADAPTORS.keys())) assert ax_id in before_keys, "Axes should be in registry after waveshow" # Verify the registry is a WeakKeyDictionary (semantics test) assert isinstance(librosa.display._WAVESHOW_ADAPTORS, weakref.WeakKeyDictionary) # Verify an adaptor is registered for this axes assert ax in librosa.display._WAVESHOW_ADAPTORS assert len(librosa.display._WAVESHOW_ADAPTORS[ax]) >= 1 def test_waveshow_update_gc_guard(): fig, ax = plt.subplots() y = np.sin(2 * np.pi * 440 * np.linspace(0, 1, 22050, endpoint=False)) adaptor = librosa.display.waveshow(y, sr=22050, ax=ax) # Simulate a dead weakref class _Tmp: pass tmp = _Tmp() adaptor._steps_ref = weakref.ref(tmp) # type: ignore[arg-type] del tmp gc.collect() assert adaptor.steps is None # Should return early without error (exercises the GC guard) adaptor.update(ax) plt.close(fig) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("axis", ["x_axis", "y_axis"]) def test_unknown_axis(S_abs, axis: str): kwargs: Dict[str, Any] = {axis: "something not in the axis map"} plt.figure() librosa.display.specshow(S_abs, **kwargs) @pytest.mark.parametrize( "data", [ np.arange(1, 10.0), # strictly positive -np.arange(1, 10.0), # strictly negative np.arange(-3, 4.0), # signed, np.arange(2, dtype=bool), ], ) # binary def test_infer_cmap_robust(data): cmap1 = librosa.display.infer_cmap(data, robust=False) cmap2 = librosa.display.infer_cmap(data, robust=True) if isinstance(cmap1, matplotlib.colors.ListedColormap) and isinstance( cmap2, matplotlib.colors.ListedColormap ): assert np.allclose(cmap1.colors, cmap2.colors) elif isinstance(cmap1, matplotlib.colors.LinearSegmentedColormap) and isinstance( cmap2, matplotlib.colors.LinearSegmentedColormap ): assert cmap1.name == cmap2.name else: assert cmap1 == cmap2 assert type(cmap1) is type(cmap2) @pytest.mark.mpl_image_compare( baseline_images=["coords"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_coords(Csync, beat_t): plt.figure() librosa.display.specshow(Csync, x_coords=beat_t, x_axis="time", y_axis="cqt_note") return plt.gcf() @pytest.mark.xfail(raises=librosa.ParameterError) def test_bad_coords(S_abs): librosa.display.specshow(S_abs, x_coords=np.arange(S_abs.shape[1] // 2)) return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["sharex_specshow_ms"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_sharex_specshow_ms(S_abs, y, sr): # Correct time range ~= 4.6 s or 4600ms # Due to shared x_axis, both plots are plotted in 's'. fig, (ax, ax2) = plt.subplots(nrows=2, figsize=(8, 8), sharex=True) librosa.display.specshow( librosa.amplitude_to_db(S_abs, ref=np.max), x_axis="time", ax=ax ) ax.set(xlabel="") # hide the x label here, which is not propagated automatically ax2.margins(x=0) librosa.display.waveshow(y, sr=sr, axis="ms", ax=ax2) ax2.set(xlabel="") # hide the x label here, which is not propagated automatically return fig @pytest.mark.mpl_image_compare( baseline_images=["sharex_waveplot_ms"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_sharex_waveplot_ms(y, sr, S_abs): # Correct time range ~= 4.6 s or 4600ms # Due to shared x_axis, both plots are plotted in 'ms'. fig, (ax, ax2) = plt.subplots(sharex=True, nrows=2, figsize=(8, 8)) ax.margins(x=0) librosa.display.waveshow(y, sr=sr, ax=ax) ax.set(xlabel="") # hide the x label here, which is not propagated automatically ax2.margins(x=0) librosa.display.specshow( librosa.amplitude_to_db(S_abs, ref=np.max), x_axis="ms", ax=ax2 ) ax2.set(xlabel="") # hide the x label here, which is not propagated automatically return fig @pytest.mark.parametrize("format_str", ["cqt_hz", "cqt_note", "vqt_hz"]) def test_axis_bound_warning(format_str): with pytest.warns(UserWarning): # set sr=22050 # fmin= 11025 # 72 bins # 12 bins per octave librosa.display.specshow( np.zeros((72, 3)), y_axis=format_str, fmin=11025, sr=22050, bins_per_octave=12, intervals="ji3", ) @pytest.mark.mpl_image_compare( baseline_images=["cqt_svara"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_display_cqt_svara(C, sr): Camp = librosa.amplitude_to_db(C, ref=np.max) fig, (ax1, ax2, ax3, ax4, ax5) = plt.subplots( nrows=5, sharex=True, figsize=(10, 10) ) librosa.display.specshow(Camp, y_axis="cqt_svara", Sa=261, ax=ax1) librosa.display.specshow(Camp, y_axis="cqt_svara", Sa=440, ax=ax2) librosa.display.specshow(Camp, y_axis="cqt_svara", Sa=261, ax=ax3) librosa.display.specshow(Camp, y_axis="cqt_svara", Sa=261, mela=1, ax=ax4) librosa.display.specshow(Camp, y_axis="cqt_svara", Sa=261, mela=1, ax=ax5) ax3.set_ylim([440, 880]) ax5.set_ylim([440, 880]) ax1.label_outer() ax2.label_outer() ax3.label_outer() ax4.label_outer() return fig @pytest.mark.mpl_image_compare( baseline_images=["fft_svara"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_display_fft_svara(S_abs, sr): fig, (ax1, ax2, ax3, ax4, ax5) = plt.subplots( nrows=5, sharex=True, figsize=(10, 10) ) librosa.display.specshow(S_abs, y_axis="fft_svara", Sa=261, ax=ax1) librosa.display.specshow(S_abs, y_axis="fft_svara", Sa=440, ax=ax2) librosa.display.specshow(S_abs, y_axis="fft_svara", Sa=261, ax=ax3) librosa.display.specshow(S_abs, y_axis="fft_svara", Sa=261, mela=1, ax=ax4) librosa.display.specshow(S_abs, y_axis="fft_svara", Sa=261, mela=1, ax=ax5) ax3.set_ylim([440, 880]) ax5.set_ylim([440, 880]) ax1.label_outer() ax2.label_outer() ax3.label_outer() ax4.label_outer() return fig @pytest.mark.mpl_image_compare( baseline_images=["nfft_odd"], extensions=["png"], style=STYLE ) def test_display_fft_odd(): fig, (ax1, ax2, ax3) = plt.subplots(nrows=3, sharex=True, figsize=(10, 10)) N1 = 8 N2 = N1 + 1 sr = N1 S = np.tile(np.arange(1 + N1 // 2), (20, 1)).T # Use the default inference librosa.display.specshow(S, x_axis="time", y_axis="fft", sr=sr, ax=ax1) # Force it to match exactly librosa.display.specshow(S, x_axis="time", y_axis="fft", sr=sr, n_fft=N1, ax=ax2) # Override with an odd number librosa.display.specshow(S, x_axis="time", y_axis="fft", sr=sr, n_fft=N2, ax=ax3) ax1.label_outer() ax2.label_outer() ax3.label_outer() return fig @pytest.mark.mpl_image_compare( baseline_images=["nfft_odd_ftempo"], extensions=["png"], style=STYLE ) def test_display_fourier_tempo_odd(): fig, (ax1, ax2, ax3) = plt.subplots(nrows=3, sharex=True, figsize=(10, 10)) N1 = 8 N2 = N1 + 1 sr = N1 S = np.tile(np.arange(1 + N1 // 2), (20, 1)).T # Use the default inference librosa.display.specshow(S, y_axis="fourier_tempo", sr=sr, ax=ax1) # Force it to match exactly librosa.display.specshow(S, y_axis="fourier_tempo", sr=sr, win_length=N1, ax=ax2) # Override with an odd number librosa.display.specshow(S, y_axis="fourier_tempo", sr=sr, win_length=N2, ax=ax3) ax1.label_outer() ax2.label_outer() ax3.label_outer() return fig @pytest.mark.parametrize( "x_axis,y_axis,xlim,ylim,out", [ (None, None, (0.0, 1.0), (0.0, 1.0), False), ("time", "linear", (0.0, 1.0), (0.0, 1.0), False), ("time", "time", (0.0, 1.0), (0.0, 2.0), False), ("chroma", "chroma", (0.0, 1.0), (0.0, 1.0), True), ("s", "ms", (0.0, 1.0), (0.0, 1.0), True), ], ) def test_same_axes(x_axis, y_axis, xlim, ylim, out): assert librosa.display.__same_axes(x_axis, y_axis, xlim, ylim) == out def test_auto_aspect(): fig, ax = plt.subplots(nrows=5) # Ensure auto aspect by default for axi in ax: axi.set(aspect="auto") X = np.zeros((12, 12)) # Different axes with incompatible types should retain auto scaling librosa.display.specshow(X, x_axis="chroma", y_axis="time", ax=ax[0]) assert ax[0].get_aspect() == "auto" # Same axes and auto_aspect=True should force equal scaling librosa.display.specshow(X, x_axis="chroma", y_axis="chroma", ax=ax[1]) assert ax[1].get_aspect() == 1.0 # Same axes and auto_aspect=False should retain auto scaling librosa.display.specshow( X, x_axis="chroma", y_axis="chroma", auto_aspect=False, ax=ax[2] ) assert ax[2].get_aspect() == "auto" # Different extents with auto_aspect=True should retain auto scaling librosa.display.specshow( X[:2, :], x_axis="chroma", y_axis="chroma", auto_aspect=True, ax=ax[3] ) assert ax[3].get_aspect() == "auto" # different axes with compatible types and auto_aspect=True should force equal scaling librosa.display.specshow(X, x_axis="time", y_axis="ms", ax=ax[4]) assert ax[4].get_aspect() == 1.0 @pytest.mark.mpl_image_compare( baseline_images=["specshow_unicode_true"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_specshow_unicode_true(C, sr): chroma = librosa.feature.chroma_cqt(C=C, sr=sr, threshold=0.9) fig, ax = plt.subplots(nrows=5, sharex=True, figsize=(10, 10)) # Hindustani, no thaat librosa.display.specshow(chroma, y_axis="chroma_h", Sa=5, ax=ax[0], unicode=True) # Hindustani, kafi thaat librosa.display.specshow( chroma, y_axis="chroma_h", Sa=5, ax=ax[1], thaat="kafi", unicode=True ) # Carnatic, mela 22 librosa.display.specshow( chroma, y_axis="chroma_c", Sa=5, ax=ax[2], mela=22, unicode=True ) # Carnatic, mela 1 librosa.display.specshow( chroma, y_axis="chroma_c", Sa=7, ax=ax[3], mela=1, unicode=True ) # Pitches librosa.display.specshow( chroma, y_axis="chroma", ax=ax[4], key="Eb:maj", unicode=True ) for axi in ax: axi.label_outer() return fig @pytest.mark.mpl_image_compare( baseline_images=["specshow_unicode_false"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_specshow_unicode_false(C, sr): chroma = librosa.feature.chroma_cqt(C=C, sr=sr, threshold=0.9) fig, ax = plt.subplots(nrows=5, sharex=True, figsize=(10, 10)) # Hindustani, no thaat librosa.display.specshow(chroma, y_axis="chroma_h", Sa=5, ax=ax[0], unicode=False) # Hindustani, kafi thaat librosa.display.specshow( chroma, y_axis="chroma_h", Sa=5, ax=ax[1], thaat="kafi", unicode=False ) # Carnatic, mela 22 librosa.display.specshow( chroma, y_axis="chroma_c", Sa=5, ax=ax[2], mela=22, unicode=False ) # Carnatic, mela 1 librosa.display.specshow( chroma, y_axis="chroma_c", Sa=7, ax=ax[3], mela=1, unicode=False ) librosa.display.specshow( chroma, y_axis="chroma", ax=ax[4], key="Eb:maj", unicode=False ) for axi in ax: axi.label_outer() return fig def test_waveshow_disconnect(y, sr): fig, ax = plt.subplots() ad = librosa.display.waveshow(y=y, sr=sr, ax=ax) assert ad.envelope is not None assert ad.steps is not None # By default, envelope should be visible and steps should not assert ad.envelope.get_visible() and not ad.steps.get_visible() # Zoom in to a 0.25 second range ax.set(xlim=[0, 0.25]) # Steps should be visible but not envelope assert ad.steps.get_visible() and not ad.envelope.get_visible() # Zoom back out ax.set(xlim=[0, 4]) assert ad.envelope.get_visible() and not ad.steps.get_visible() # Disconnect ad.disconnect() # Zoom back in to a 0.25 second range ax.set(xlim=[0, 0.25]) # Envelope should now still be visible assert ad.envelope.get_visible() and not ad.steps.get_visible() def test_waveshow_deladaptor(y, sr): fig, ax = plt.subplots() ad = librosa.display.waveshow(y=y, sr=sr, ax=ax) envelope, steps = ad.envelope, ad.steps # These should be live for the rest of the tests to function assert envelope is not None assert steps is not None # By default, envelope should be visible and steps should not assert envelope.get_visible() and not steps.get_visible() # Zoom in to a 0.25 second range ax.set(xlim=[0, 0.25]) # Steps should be visible but not envelope assert steps.get_visible() and not envelope.get_visible() # Zoom back out ax.set(xlim=[0, 4]) assert envelope.get_visible() and not steps.get_visible() # Disconnect ad.disconnect(strict=True) # Zoom back in to a 0.25 second range ax.set(xlim=[0, 0.25]) # Envelope should now still be visible assert envelope.get_visible() and not steps.get_visible() @pytest.mark.mpl_image_compare( baseline_images=["specshow_vqt"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_specshow_vqt(C): fig, ax = plt.subplots(nrows=4, figsize=(12, 10)) librosa.display.specshow(C, y_axis="vqt_hz", intervals="ji5", ax=ax[0]) librosa.display.specshow(C, y_axis="vqt_note", intervals="ji5", ax=ax[1]) librosa.display.specshow(C, y_axis="vqt_fjs", intervals="ji5", ax=ax[2]) librosa.display.specshow( C, y_axis="vqt_fjs", intervals="ji5", ax=ax[3], unicode=False ) for _ax in ax: _ax.set(ylim=[55, 165]) return fig @pytest.mark.xfail(raises=librosa.ParameterError) def test_chromafjs_badintervals(): formatter = librosa.display.ChromaFJSFormatter(intervals=dict()) @pytest.mark.xfail(raises=librosa.ParameterError) def test_chromafjs_badbpo(): formatter = librosa.display.ChromaFJSFormatter( intervals="ji3", bins_per_octave=None ) @pytest.mark.mpl_image_compare( baseline_images=["chroma_fjs"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_specshow_chromafjs(C, sr): # This isn't a VQT chroma, but that's not important here chroma = librosa.feature.chroma_cqt(C=C, sr=sr, threshold=0.9) intervals = librosa.plimit_intervals(primes=[3, 5]) fig, ax = plt.subplots(nrows=2, figsize=(12, 8)) librosa.display.specshow(chroma, y_axis="chroma_fjs", intervals="ji5", ax=ax[0]) librosa.display.specshow(chroma, y_axis="chroma_fjs", intervals=intervals, ax=ax[1]) return fig @pytest.mark.xfail(raises=librosa.ParameterError) def test_vqt_hz_nointervals(C, sr): librosa.display.specshow(C, sr=sr, y_axis="vqt_hz") @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("vscale", ["dBFS[1]", "dBFS[power,1]"]) def test_parse_vscale_dbfs_ref(vscale): # This should raise an error because a reference value is # not allowed with dBFS librosa.display.__parse_vscale(vscale) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("vscale", ["bad string", "dB[gibberish]", "dBFS[gibberish]"]) def test_parse_vscale_fail(vscale): librosa.display.__parse_vscale(vscale) @pytest.mark.parametrize( "vscale, mode, scale_type, ref", [ ("dBFS", "dBFS", "amplitude", "max"), ("dBFS[power]", "dBFS", "power", "max"), ("dB", "dB", "amplitude", None), ("dB[2]", "dB", "amplitude", 2), ("dB[1e-1]", "dB", "amplitude", 0.1), ("dB[power]", "dB", "power", None), ("dB[power,2]", "dB", "power", 2), ("dB[power,1e-1]", "dB", "power", 0.1), ], ) def test_parse_vscale(vscale, mode, scale_type, ref): assert librosa.display.__parse_vscale(vscale) == (mode, scale_type, ref) @pytest.mark.mpl_image_compare( baseline_images=["specshow_vscale"], extensions=["png"], style=STYLE ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_specshow_vscale(S): fig, ax = plt.subplots(nrows=3, ncols=2, sharex=True, sharey=True, figsize=(12, 12)) # first column is dB, dBFS, dB with ref value i1 = librosa.display.specshow( S, vscale="dB", y_axis="log", x_axis="time", ax=ax[0, 0] ) fig.colorbar(i1, ax=ax[0, 0]) ax[0, 0].set_title("dB") i2 = librosa.display.specshow( S, vscale="dBFS", y_axis="log", x_axis="time", ax=ax[1, 0] ) fig.colorbar(i2, ax=ax[1, 0]) ax[1, 0].set_title("dBFS") i3 = librosa.display.specshow( S, vscale="dB[1e-2]", y_axis="log", x_axis="time", ax=ax[2, 0] ) fig.colorbar(i3, ax=ax[2, 0]) ax[2, 0].set_title("dB, ref=1e-2") # second column is dB[power], dBFS[power], dB[power] with ref value i4 = librosa.display.specshow( S, vscale="dB[power]", y_axis="log", x_axis="time", ax=ax[0, 1] ) fig.colorbar(i4, ax=ax[0, 1]) ax[0, 1].set_title("dB power") i5 = librosa.display.specshow( S, vscale="dBFS[power]", y_axis="log", x_axis="time", ax=ax[1, 1] ) fig.colorbar(i5, ax=ax[1, 1]) ax[1, 1].set_title("dBFS power") i6 = librosa.display.specshow( S, vscale="dB[power,1e-2]", y_axis="log", x_axis="time", ax=ax[2, 1] ) fig.colorbar(i6, ax=ax[2, 1]) ax[2, 1].set_title("dB power, ref=1e-2") for _ax in ax.flat: _ax.label_outer() return fig @pytest.mark.mpl_image_compare( baseline_images=["specshow_vscale_phase"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) @pytest.mark.usefixtures('text_placeholders') def test_specshow_vscale_phase(): # Create a chirp sr = 22050 y = librosa.chirp(fmin=110, fmax=880, duration=1, sr=sr, linear=False) S = librosa.stft(y, n_fft=2048, hop_length=512) alpha = np.abs(S) alpha /= np.max(alpha) # normalize to [0, 1] fig, ax = plt.subplots( nrows=1, ncols=3, sharex=False, sharey=False, gridspec_kw={"hspace": 0.8}, figsize=(12, 4), ) # phase, phase difference, and phase difference transpose # we use alpha channels here to avoid test failures for unstable phase estimates in quiet regions i7 = librosa.display.specshow( S, vscale="phase", y_axis="log", x_axis="time", ax=ax[0], alpha=alpha ) fig.colorbar(i7, ax=ax[0]) ax[0].set_title("phase") i8 = librosa.display.specshow( S, vscale="dphase", y_axis="log", x_axis="time", ax=ax[1], alpha=alpha ) fig.colorbar(i8, ax=ax[1]) ax[1].set_title("dphase") i9 = librosa.display.specshow( S.T, vscale="dphase_t", x_axis="log", y_axis="time", ax=ax[2], alpha=alpha.T ) fig.colorbar(i9, ax=ax[2]) ax[2].set_title("dphase_t") return fig @pytest.mark.mpl_image_compare( baseline_images=["colorbar_db"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_colorbar_db(S): fig, ax = plt.subplots(nrows=2) i1 = librosa.display.specshow(S, vscale="dB", y_axis="log", x_axis="time", ax=ax[0]) librosa.display.colorbar_db(i1, ax=ax[0]) i2 = librosa.display.specshow( S, vscale="dBFS", y_axis="log", x_axis="time", ax=ax[1] ) librosa.display.colorbar_db(i2, ax=ax[1], label="dBFS") return fig @pytest.mark.mpl_image_compare( baseline_images=["colorbar_phase"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_colorbar_phase(S): alpha = np.abs(S) alpha /= np.max(alpha) fig, ax = plt.subplots(nrows=2) i1 = librosa.display.specshow( S, vscale="phase", y_axis="log", x_axis="time", alpha=alpha, ax=ax[0] ) librosa.display.colorbar_phase(i1, ax=ax[0]) i2 = librosa.display.specshow( S, vscale="dphase", y_axis="log", x_axis="time", alpha=alpha, ax=ax[1] ) librosa.display.colorbar_phase(i2, ax=ax[1], label="Δ radians") return fig @pytest.mark.mpl_image_compare( baseline_images=["diverging_slopes"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_diverging_scales(S_signed): # Cases to test: # 0. Diverging scale with default cmap_div (auto norm) # 1. Diverging scale with explicit cmap override (no norm) # 2. Diverging scale with specified cmap_div (auto norm) # 3. Inferred diverging scale with default cmap_div and vmin/vmax (auto norm, truncated) # 4. Explicit diverging scale with vmin/vmax (no norm, truncated) fig, ax = plt.subplots(nrows=3, ncols=2, figsize=(12, 12), sharex=True, sharey=True) # Diverging scale with default cmap_div (auto norm) i1 = librosa.display.specshow(S_signed, y_axis="log", x_axis="time", ax=ax[0, 0]) ax[0, 0].set_title("Default cmap_div (auto norm)") fig.colorbar(i1, ax=ax[0, 0]) # Diverging scale with explicit cmap override (no norm) i2 = librosa.display.specshow( S_signed, y_axis="log", x_axis="time", ax=ax[0, 1], cmap="PuOr_r", ) ax[0, 1].set_title("Explicit cmap override (no norm)") fig.colorbar(i2, ax=ax[0, 1]) # Diverging scale with specified cmap_div (auto norm) i3 = librosa.display.specshow( S_signed, y_axis="log", x_axis="time", ax=ax[1, 0], cmap_div="Spectral_r" ) ax[1, 0].set_title("Specified cmap_div (auto norm)") fig.colorbar(i3, ax=ax[1, 0]) # Inferred diverging scale with default cmap_div and vmin/vmax (auto norm, truncated) vmin = -10 vmax = 30 i4 = librosa.display.specshow( S_signed, y_axis="log", x_axis="time", ax=ax[2, 0], vmin=vmin, vmax=vmax, cmap_div=matplotlib.colormaps["coolwarm"], ) ax[2, 0].set_title("Inferred cmap_div with vmin/vmax (auto norm, truncated)") fig.colorbar(i4, ax=ax[2, 0]) # Explicit diverging scale with vmin/vmax (no norm, truncated) i5 = librosa.display.specshow( S_signed, y_axis="log", x_axis="time", ax=ax[1, 1], vmin=vmin, vmax=vmax, cmap="Spectral_r", ) ax[1, 1].set_title("Explicit cmap_div with vmin/vmax (no norm, truncated)") fig.colorbar(i5, ax=ax[1, 1]) # Hide the last axis ax[2, 1].axis("off") for axi in ax.flat: axi.label_outer() return fig @pytest.mark.mpl_image_compare( baseline_images=["oct3"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_oct3(S_abs, C): fig, ax = plt.subplots(nrows=2, ncols=2) # STFT, Mel # CQT, VQT librosa.display.specshow(S_abs, vscale="dBFS", y_axis="log_oct3", ax=ax[0, 0]) M = librosa.feature.melspectrogram(S=S_abs**2) librosa.display.specshow(M, vscale="dBFS[power]", y_axis="mel_oct3", ax=ax[0, 1]) librosa.display.specshow(C, vscale="dBFS", y_axis="cqt_oct3", ax=ax[1, 0]) librosa.display.specshow( C, vscale="dBFS", y_axis="vqt_oct3", ax=ax[1, 1], intervals="equal" ) # Put the ticks on the right just to reduce crowding ax[0, 1].yaxis.tick_right() ax[1, 1].yaxis.tick_right() return fig @pytest.mark.mpl_image_compare( baseline_images=["oct3_minor"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_oct3_minor(S_abs, C): fig, ax = plt.subplots(nrows=2, ncols=2) # STFT, Mel # CQT, VQT librosa.display.specshow(S_abs, vscale="dBFS", y_axis="log_oct3", ax=ax[0, 0]) M = librosa.feature.melspectrogram(S=S_abs**2) librosa.display.specshow(M, vscale="dBFS[power]", y_axis="mel_oct3", ax=ax[0, 1]) librosa.display.specshow(C, vscale="dBFS", y_axis="cqt_oct3", ax=ax[1, 0]) librosa.display.specshow( C, vscale="dBFS", y_axis="vqt_oct3", ax=ax[1, 1], intervals="equal" ) # Put the ticks on the right just to reduce crowding ax[0, 1].yaxis.tick_right() ax[1, 1].yaxis.tick_right() for axi in ax.flat: axi.set_ylim([200, 600]) return fig @pytest.mark.mpl_image_compare( baseline_images=["wavebars"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_wavebars(y, sr): fig, ax = plt.subplots(nrows=6, layout="tight", figsize=(8, 12)) librosa.display.wavebars(y, sr=sr, ax=ax[0], label="100") librosa.display.wavebars( y, sr=sr, color="C1", n_bars=150, rounding_ratio=0, ax=ax[1], label="150 square" ) librosa.display.wavebars( y, sr=sr, color="C2", n_bars=50, ax=ax[2], invert=True, label="50 inverted" ) librosa.display.wavebars( y, sr=sr, color="C3", gap_ratio=0, ax=ax[3], label="no gap" ) librosa.display.wavebars( y, sr=sr, color="C4", offset=30, rounding_ratio=0.3, invert=True, invert_color="#2d2d2d", ax=ax[4], label="offset 30, invert dark", ) librosa.display.wavebars(y, sr=sr, ax=ax[5], color="C5", axis="ms", label="time_ms") for axi in ax.flat: axi.legend() return fig @pytest.mark.mpl_image_compare( baseline_images=["wavebars_transpose"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_wavebars_transpose(y, sr): fig, ax = plt.subplots(ncols=6, layout="tight", figsize=(12, 8)) librosa.display.wavebars(y, sr=sr, ax=ax[0], label="100", transpose=True) librosa.display.wavebars( y, sr=sr, color="C1", n_bars=150, rounding_ratio=0, transpose=True, ax=ax[1], label="150 square", ) librosa.display.wavebars( y, sr=sr, color="C2", n_bars=50, transpose=True, ax=ax[2], invert=True, label="50 inverted", ) librosa.display.wavebars( y, sr=sr, color="C3", gap_ratio=0, transpose=True, ax=ax[3], label="no gap" ) librosa.display.wavebars( y, sr=sr, color="C4", offset=30, transpose=True, rounding_ratio=0.3, invert=True, invert_color="#2d2d2d", ax=ax[4], label="offset 30, invert dark", ) librosa.display.wavebars( y, sr=sr, ax=ax[5], color="C5", axis="ms", transpose=True, label="time_ms" ) for axi in ax.flat: axi.legend() return fig @pytest.mark.mpl_image_compare( baseline_images=["wavef0"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_wavef0(y, sr): fig, ax = plt.subplots(nrows=2, figsize=(8, 6)) f0, _, _ = librosa.pyin(y, fmin=float(librosa.note_to_hz("C2")), fmax=float(librosa.note_to_hz("C7")), sr=sr) # Waveform with f0 librosa.display.wavef0(y, sr=sr, f0=f0, ax=ax[0], label='waveshow') ax[0].legend(loc='lower right') # Waveform with f0 and pitch librosa.display.wavef0(y, sr=sr, f0=f0, method='wavebars', freq_axis='cqt_hz', color='C1', ax=ax[1], label='wavebars') ax[1].legend(loc='lower right') return fig @pytest.mark.xfail(raises=librosa.ParameterError) def test_legend_for_axes_no_axes(): fig = plt.figure() librosa.display.legend_for_axes(fig=fig) def test_legend_for_axes_current(): fig, ax = plt.subplots(nrows=2, ncols=2) ax[0, 0].plot(np.arange(4), label="line") leg = librosa.display.legend_for_axes() assert leg is not None assert leg.figure is fig def test_legend_for_axes_scalar(): fig, ax = plt.subplots() ax.plot(np.arange(4), label="line") leg = librosa.display.legend_for_axes(axes=ax) assert leg is not None assert leg.figure is fig @pytest.mark.xfail(raises=librosa.ParameterError) def test_legend_for_axes_mismatched_figures(): fig1, ax1 = plt.subplots() fig2, ax2 = plt.subplots() librosa.display.legend_for_axes([ax1, ax2]) def test_legend_for_axes_explicit_bbox(): fig, ax = plt.subplots() ax.plot([0, 1], [0, 1], label="a") leg = librosa.display.legend_for_axes( [ax], loc="center", bbox_to_anchor=(0.5, 0.5, 0.2, 0.2) ) assert leg is not None @pytest.mark.mpl_image_compare( baseline_images=["legend_for_axes_right"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_legend_for_axes_right(): fig, axes = plt.subplots(nrows=3, figsize=(8, 6), sharex=True) for i, ax in enumerate(np.ravel(axes)): ax.plot([0, 1], [i, i + 1], label=f"line-{i}", color=f"C{i}") fig.subplots_adjust(right=0.8) librosa.display.legend_for_axes(axes=axes, loc="outside center right") return fig @pytest.mark.mpl_image_compare( baseline_images=["wavef0_transpose"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_wavef0_transpose(y, sr): fig, ax = plt.subplots(ncols=2, figsize=(6, 8)) f0, _, _ = librosa.pyin(y, fmin=float(librosa.note_to_hz("C2")), fmax=float(librosa.note_to_hz("C7")), sr=sr) # Waveform with f0 librosa.display.wavef0(y, sr=sr, f0=f0, ax=ax[0], label='waveshow', transpose=True) ax[0].legend(loc='lower right') # Waveform with f0 and pitch librosa.display.wavef0(y, sr=sr, f0=f0, method='wavebars', freq_axis='cqt_hz', color='C1', ax=ax[1], label='wavebars', transpose=True) ax[1].legend(loc='lower right') return fig @pytest.mark.mpl_image_compare( baseline_images=["legend_for_axes_left"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_legend_for_axes_left(): fig, axes = plt.subplots(nrows=2, figsize=(8, 4), sharex=True) for i, ax in enumerate(np.ravel(axes)): ax.plot([0, 1], [i, i + 1], label=f"line-{i}", color=f"C{i}") fig.subplots_adjust(left=0.25) librosa.display.legend_for_axes(axes=axes, loc="outside center left") return fig @pytest.mark.xfail(raises=librosa.ParameterError) def test_wavef0_bad_method(y, sr): f0, _, _ = librosa.pyin(y, fmin=float(librosa.note_to_hz("C2")), fmax=float(librosa.note_to_hz("C7")), sr=sr) librosa.display.wavef0(y, f0=f0, sr=sr, method='bad_method') @pytest.mark.parametrize( "transpose,values", [ ( False, np.array([[0.0, -12.0], [1.0, 0.0], [2.0, 7.0], [3.0, 12.0]], dtype=float), ), ( True, np.array([[-12.0, 0.0], [0.0, 1.0], [7.0, 2.0], [12.0, 3.0]], dtype=float), ), ], ) def test_transformf0_roundtrip(transpose, values): f0 = np.array([100.0, 110.0, 120.0, 130.0], dtype=float) trans = librosa.display.Transformf0( f0=f0, sr=1, hop_length=1, bins_per_octave=12, norm=2.0, transpose=transpose, ) forward = trans.transform(values) recovered = trans.inverted().transform(forward) assert np.allclose(recovered, values, rtol=1e-12, atol=1e-12) @pytest.mark.parametrize('f0', [np.zeros(5), np.linspace(-10, -5, num=5), np.linspace(-10, 10, num=5)]) @pytest.mark.xfail(raises=librosa.ParameterError) def test_transformf0_bad_f0(f0): librosa.display.Transformf0(f0=f0, sr=1, hop_length=1) @pytest.mark.mpl_image_compare( baseline_images=["legend_for_axes_above"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_legend_for_axes_above(): fig, axes = plt.subplots(ncols=3, figsize=(8, 4), sharey=True) for i, ax in enumerate(np.ravel(axes)): ax.plot([0, 1], [i, i + 1], label=f"line-{i}", color=f"C{i}") librosa.display.legend_for_axes(axes=axes, loc="outside upper center", ncol=3) return fig @pytest.mark.mpl_image_compare( baseline_images=["legend_for_axes_below"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_legend_for_axes_below(): fig, axes = plt.subplots(ncols=3, figsize=(8, 4), sharey=True) for i, ax in enumerate(np.ravel(axes)): ax.plot([0, 1], [i, i + 1], label=f"line-{i}", color=f"C{i}") librosa.display.legend_for_axes(axes=axes, loc="outside lower center", ncol=3) return fig @pytest.mark.mpl_image_compare( baseline_images=["legend_for_axes_default_1d"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_legend_for_axes_default_1d(): fig, axes = plt.subplots(nrows=3, figsize=(8, 6), sharex=True) for i, ax in enumerate(np.ravel(axes)): ax.plot([0, 1], [i, i + 1], label=f"line-{i}", color=f"C{i}") librosa.display.legend_for_axes(axes=axes) return fig @pytest.mark.mpl_image_compare( baseline_images=["legend_for_axes_default_row"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_legend_for_axes_default_row(): fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(8, 4), sharey=True) for i, ax in enumerate(np.ravel(axes)): ax.plot([0, 1], [i, i + 1], label=f"line-{i}", color=f"C{i}") axes = np.asarray(axes)[np.newaxis, :] librosa.display.legend_for_axes(axes=axes) return fig @pytest.mark.mpl_image_compare( baseline_images=["legend_for_axes_default_col"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_legend_for_axes_default_col(): fig, axes = plt.subplots(nrows=3, ncols=1, figsize=(8, 6), sharex=True) for i, ax in enumerate(np.ravel(axes)): ax.plot([0, 1], [i, i + 1], label=f"line-{i}", color=f"C{i}") axes = np.asarray(axes)[:, np.newaxis] librosa.display.legend_for_axes(axes=axes, loc="outside center right") return fig @pytest.mark.mpl_image_compare( baseline_images=["legend_for_axes_default_grid"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_legend_for_axes_default_grid(): fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(8, 6), sharex=True, sharey=True) for i, ax in enumerate(np.ravel(axes)): ax.plot([0, 1], [i, i + 1], label=f"line-{i}", color=f"C{i}") librosa.display.legend_for_axes(axes=axes) return fig def test_squeeze_shape(): assert librosa.display._squeeze_shape((3,)) == (3,) assert librosa.display._squeeze_shape((1, 3)) == (3,) assert librosa.display._squeeze_shape((3, 1)) == (3,) assert librosa.display._squeeze_shape((1, 3, 1, 2, 1)) == (3, 2) assert librosa.display._squeeze_shape((1, 1, 1)) == () @pytest.mark.parametrize( "func,dims,badprops", [ ("waveshow", 1, []), ("wavebars", 1, []), ("specshow", 2, ["color"]), ], ) def test_resolve_multiplot(func, dims, badprops): function, dims_out, badprops_out = librosa.display._resolve_multiplot(func) assert callable(function) assert dims_out == dims assert badprops_out == badprops @pytest.mark.xfail(raises=librosa.ParameterError) def test_resolve_multiplot_bad(): librosa.display._resolve_multiplot("bogus") # type: ignore @pytest.mark.parametrize( "data,dims,orient,axshape,nrows,ncols,multi_input", [ ((np.zeros((3, 10)),), 1, "v", (3,), 3, 1, False), ((np.zeros((3, 10)),), 1, "h", (3,), 1, 3, False), ((np.zeros((2, 3, 10)),), 1, "v", (2, 3), 2, 3, False), ((np.zeros((2, 3, 4, 5)),), 2, "v", (2, 3), 2, 3, False), ((np.zeros(10), np.zeros(10), np.zeros(10)), 1, "v", (3,), 3, 1, True), ((np.zeros(10), np.zeros(10), np.zeros(10)), 1, "h", (3,), 1, 3, True), ((np.zeros((4, 5)), np.zeros((4, 5))), 2, "v", (2,), 2, 1, True), ], ) def test_mp_get_layout(data, dims, orient, axshape, nrows, ncols, multi_input): axshape_out, nrows_out, ncols_out, multi_input_out = librosa.display._mp_get_layout( data, dims, orient ) assert axshape_out == axshape assert nrows_out == nrows assert ncols_out == ncols assert multi_input_out is multi_input @pytest.mark.xfail(raises=librosa.ParameterError) def test_mp_get_layout_bad_orient(): librosa.display._mp_get_layout((np.zeros((3, 10)),), dims=1, orient="q") # type: ignore @pytest.mark.xfail(raises=librosa.ParameterError) def test_mp_get_layout_no_data(): librosa.display._mp_get_layout(tuple(), dims=1, orient="v") @pytest.mark.xfail(raises=librosa.ParameterError) def test_mp_get_layout_bad_axshape(): librosa.display._mp_get_layout((np.zeros((2, 3, 4, 10)),), dims=1, orient="v") @pytest.mark.parametrize( "shape,orient,output_shape,axes_shape", [ ((3,), "v", (3,), (3, 1)), ((3,), "h", (3,), (1, 3)), ((2, 2), "v", (2, 2), (2, 2)), ((2, 2), "h", (2, 2), (2, 2)), ], ) def test_mp_setup_axes_create(shape, orient, output_shape, axes_shape): if len(shape) == 1: if orient == "v": nrows, ncols = (shape[0], 1) elif orient == "h": nrows, ncols = (1, shape[0]) else: nrows, ncols = shape fig, axes, out_shape = librosa.display._mp_setup_axes( axes=None, fig=None, nrows=nrows, ncols=ncols, axshape=shape, orient=orient, sharex=True, sharey=True, ) assert fig is not None assert axes.shape == axes_shape assert out_shape == output_shape def test_mp_setup_axes_with_fig(): fig = plt.figure() fig_out, axes, out_shape = librosa.display._mp_setup_axes( axes=None, fig=fig, fig_kw=None, nrows=2, ncols=1, axshape=(2,), orient="v", sharex=True, sharey=True, ) assert fig_out is fig assert axes.shape == (2, 1) assert out_shape == (2,) @pytest.mark.parametrize( "orient,axes_in_shape,axes_out_shape,output_shape", [ ("v", (3,), (3, 1), (3,)), ("h", (3,), (1, 3), (3,)), ("v", (2, 2), (2, 2), (2, 2)), ], ) def test_mp_setup_axes_array_input(orient, axes_in_shape, axes_out_shape, output_shape): fig, axes_in = ( plt.subplots(*axes_in_shape) if len(axes_in_shape) == 2 else plt.subplots(axes_in_shape[0]) ) fig_out, axes_out, out_shape = librosa.display._mp_setup_axes( axes=np.asarray(axes_in), fig=None, fig_kw=None, nrows=axes_out_shape[0], ncols=axes_out_shape[1], axshape=output_shape, orient=orient, sharex=True, sharey=True, ) assert fig_out is fig assert axes_out.shape == axes_out_shape assert out_shape == output_shape def test_mp_setup_axes_scalar_input(): fig, ax = plt.subplots() fig_out, axes_out, out_shape = librosa.display._mp_setup_axes( axes=ax, fig=None, fig_kw=None, nrows=1, ncols=1, axshape=tuple(), orient="v", sharex=True, sharey=True, ) assert fig_out is fig assert axes_out.shape == (1, 1) assert out_shape == tuple() @pytest.mark.xfail(raises=librosa.ParameterError) def test_mp_setup_axes_bad_shape(): fig, axes = plt.subplots(nrows=2) librosa.display._mp_setup_axes( axes=np.asarray(axes), fig=None, fig_kw=None, nrows=3, ncols=1, axshape=(3,), orient="v", sharex=True, sharey=True, ) def test_mp_setup_axes_single(): fig, ax = plt.subplots() fig_out, axes_out, out_shape = librosa.display._mp_setup_axes( axes=ax, fig=None, fig_kw=None, nrows=1, ncols=1, axshape=tuple(), orient="v", sharex=True, sharey=True, ) assert fig_out is fig assert axes_out.shape == (1, 1) assert out_shape == tuple() def test_mp_setup_labels_none(): labels = librosa.display._mp_setup_labels(None, (2, 2)) assert labels.shape == (2, 2) assert labels.dtype == object # Comparison to None here is vectorized, so == is correct, not `is`. assert np.all(labels == None) def test_mp_setup_labels_values(): labels = librosa.display._mp_setup_labels(["a", "b", "c"], (3,)) assert np.array_equal(labels, np.asarray(["a", "b", "c"], dtype=object)) @pytest.mark.xfail(raises=ValueError) def test_mp_setup_labels_bad_shape(): librosa.display._mp_setup_labels(["a", "b"], (2, 2)) def test_mp_setup_prop_group_none(): groups = librosa.display._mp_setup_prop_group(None, (2, 2)) assert np.array_equal(groups, np.array([[0, 1], [2, 3]])) def test_mp_setup_prop_group_false(): groups = librosa.display._mp_setup_prop_group(False, (2, 2)) assert np.array_equal(groups, np.array([[0, 1], [2, 3]])) def test_mp_setup_prop_group_true(): groups = librosa.display._mp_setup_prop_group(True, (2, 2)) assert np.array_equal(groups, np.ones((2, 2), dtype=int)) def test_mp_setup_prop_group_row(): groups = librosa.display._mp_setup_prop_group("row", (2, 3)) assert np.array_equal(groups, np.array([[0, 0, 0], [1, 1, 1]])) def test_mp_setup_prop_group_col(): groups = librosa.display._mp_setup_prop_group("col", (2, 3)) assert np.array_equal(groups, np.array([[0, 1, 2], [0, 1, 2]])) def test_mp_setup_prop_group_custom(): groups = librosa.display._mp_setup_prop_group([0, 1, 0, 1], (2, 2)) assert np.array_equal(groups, np.array([[0, 1], [0, 1]])) @pytest.mark.xfail(raises=librosa.ParameterError) def test_mp_setup_prop_group_bad_shape(): librosa.display._mp_setup_prop_group([0, 1, 2], (2, 2)) def test_mp_setup_properties_shared(): prop_group = np.array([[0, 1], [0, 1]]) properties = librosa.display._mp_setup_properties(prop_group, [], None) assert properties.shape == prop_group.shape assert properties[0, 0] == properties[1, 0] assert properties[0, 1] == properties[1, 1] assert properties[0, 0] is not properties[0, 1] def test_mp_setup_properties_badprops(): prop_group = np.array([[0, 1]]) properties = librosa.display._mp_setup_properties(prop_group, ["color"], None) for prop in properties.flat: assert "color" not in prop # type:ignore[operator] @pytest.mark.mpl_image_compare( baseline_images=["multiplot_wave_constructed_axes_stacked"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) @pytest.mark.usefixtures('text_placeholders') def test_multiplot_wave_constructed_axes_stacked(y, sr): y_stack = np.stack([y, y[::-1]]) librosa.display.multiplot( "waveshow", y_stack, sr=sr, sharex=True, sharey=True, share_properties="row", labels=["forward", "backward"], ) return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["multiplot_wave_constructed_axes_variadic"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) @pytest.mark.usefixtures('text_placeholders') def test_multiplot_wave_constructed_axes_variadic(y, sr): librosa.display.multiplot( "waveshow", y, y[::-1], sr=sr, sharex=True, sharey=True, share_properties="row", labels=["forward", "backward"], ) return plt.gcf() @pytest.mark.mpl_image_compare( baseline_images=["multiplot_wave_existing_axes_stacked"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) @pytest.mark.usefixtures('text_placeholders') def test_multiplot_wave_existing_axes_stacked(y, sr): y_stack = np.stack([y, y[::-1]]) fig, ax = plt.subplots(nrows=2, sharex=True, sharey=True, figsize=(8, 6)) out = librosa.display.multiplot( "waveshow", y_stack, axes=ax, sr=sr, share_properties="row", labels=["forward", "backward"], ) assert out.shape == ax.shape return fig @pytest.mark.mpl_image_compare( baseline_images=["multiplot_img_existing_axes_variadic"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) @pytest.mark.usefixtures('text_placeholders') def test_multiplot_img_existing_axes_variadic(y, sr): y_stack = np.stack([y, y[::-1]]) D = np.abs(librosa.stft(y_stack)) fig, ax = plt.subplots(nrows=2, sharex=True, sharey=True, figsize=(8, 6)) out = librosa.display.multiplot( "specshow", D[0], D[1], axes=ax, x_axis="time", y_axis="log", share_properties=False, titles=["forward", "backward"], ) assert out.shape == ax.shape librosa.display.colorbar_db(out[0], ax=ax) return fig @pytest.mark.mpl_image_compare( baseline_images=["multiplot_axes_slices_mixed"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) @pytest.mark.usefixtures('text_placeholders') def test_multiplot_axes_slices_mixed(y, sr): y_stack = np.stack([y, y[::-1]]) D = np.abs(librosa.stft(y_stack)) fig, ax = plt.subplots(nrows=3, ncols=2, sharex="row", figsize=(10, 8)) out_wave = librosa.display.multiplot( "waveshow", y_stack, axes=ax[0], sr=sr, sharex=True, sharey=True, share_properties="row", labels=["forward", None], ) out_bars = librosa.display.multiplot( "wavebars", y_stack, axes=ax[1], sr=sr, sharex=True, sharey=True, share_properties="row", labels=[None, "backward"], ) out_img = librosa.display.multiplot( "specshow", D, axes=ax[2], sr=sr, x_axis="time", y_axis="log", sharex=True, sharey=True, share_properties=False, titles=["forward", None], ) assert out_wave.shape == ax[0].shape assert out_bars.shape == ax[1].shape assert out_img.shape == ax[2].shape for row in ax: for axi in np.ravel(row): axi.label_outer() return fig @pytest.mark.mpl_image_compare( baseline_images=["highlight_spectrum"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) @pytest.mark.usefixtures('text_placeholders') def test_highlight_spectrum(S_abs, y, sr): fig, ax = plt.subplots(nrows=3, ncols=2, sharex=True, sharey=True) f0, _, _ = librosa.pyin(y, fmin=float(librosa.note_to_hz("C2")), fmax=float(librosa.note_to_hz("C7")), sr=sr) times = librosa.times_like(f0, sr=sr) # Column 1: dark color spectrogram # Column 2: light color spectrogram # Row 1: auto highlight # Row 2: auto highlight with specified colors # Row 3: explicit highlight for i in range(3): librosa.display.specshow(S_abs, x_axis='time', y_axis='log', cmap='magma', vscale='dBFS', ax=ax[i, 0]) librosa.display.specshow(S_abs, x_axis='time', y_axis='log', cmap='magma_r', vscale='dBFS', ax=ax[i, 1]) for j in range(2): hl1 = librosa.display.highlight(ax=ax[0, j], linewidth=5, alpha=1.0) ax[0, j].plot(times, f0, path_effects=hl1) hl2 = librosa.display.highlight(ax=ax[1, j], linewidth=5, alpha=1.0, bright_color="green", dark_color="pink") ax[1, j].plot(times, f0, path_effects=hl2) hl3 = librosa.display.highlight(ax=ax[2, j], linewidth=5, alpha=1.0, color="yellow") ax[2, j].plot(times, f0, path_effects=hl3) for axi in ax.flat: axi.label_outer() return fig @pytest.mark.mpl_image_compare( baseline_images=["highlight_axes"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) @pytest.mark.usefixtures('text_placeholders') def test_highlight_axescolor(): fig, ax = plt.subplots(nrows=3, sharex=True, sharey=True) x = np.linspace(0, 5, num=100) y = x * np.exp(-x**2) # First axes has light face color, do nothing # Second axes has a dark face color ax[1].set_facecolor((0.1, 0.1, 0.1)) # Third axes has a dark but transparent facecolor ax[2].set_facecolor((0, 0, 0, 0)) hl0 = librosa.display.highlight(ax=ax[0], linewidth=5) ax[0].plot(x, y, path_effects=hl0) hl1 = librosa.display.highlight(ax=ax[1], linewidth=5) ax[1].plot(x, y, path_effects=hl1) hl2 = librosa.display.highlight(ax=ax[2], linewidth=5) ax[2].plot(x, y, path_effects=hl2) return fig @pytest.mark.mpl_image_compare( baseline_images=["highlight_artist"], extensions=["png"], style=STYLE, ) @pytest.mark.xfail(OLD_FT, reason=f"freetype version < {FT_VERSION}", strict=False) def test_highlight_artist(): fig, ax = plt.subplots() x = np.linspace(0, 5, num=100) y = x * np.exp(-x**2) hl = librosa.display.highlight(ax=ax, linewidth=5) ax.plot(x, y, path_effects=hl, label='path_effects=') lines2 = ax.plot(x, 1 + y, label='highlight(artist)')[0] librosa.display.highlight(artist=lines2, linewidth=5) hl2 = librosa.display.highlight(linewidth=5) ax.plot(x, 2 + y, path_effects=hl2, label='path_effects= + gca') ax.legend(loc='upper right') return fig @pytest.mark.mpl_image_compare( baseline_images=["boolean_norm"], extensions=["png"], style=STYLE, ) @pytest.mark.usefixtures('text_placeholders') def test_specshow_boolean_norm(S_abs): fig, ax = plt.subplots(nrows=4, sharex=True, sharey=True) # First draw a sequential image with the same colormap as # the boolean default. # This case hits norm_cmap is None (vscale=None) # and cmap is not provided, so we have to infer i1 = librosa.display.specshow( librosa.amplitude_to_db(S_abs), y_axis="log_oct3", x_axis='time', ax=ax[0], cmap_seq='gray_r', cmap_bool='gray_r', ) fig.colorbar(i1) # Now do the same thing, but explicitly using the same # colormap objects for seq and bool cmap = mcm['Reds'] i2 = librosa.display.specshow( librosa.amplitude_to_db(S_abs), y_axis="log_oct3", x_axis='time', ax=ax[1], cmap_seq=cmap, cmap_bool=cmap, ) fig.colorbar(i2) # Now use binary data for input i3 = librosa.display.specshow( S_abs > 0.5, y_axis="log_oct3", x_axis='time', ax=ax[2], ) fig.colorbar(i3) # And finally test the override case i4 = librosa.display.specshow( S_abs > 0.5, y_axis="log_oct3", x_axis='time', ax=ax[3], cmap=None, cmap_seq='gray_r', cmap_bool='gray_r', ) fig.colorbar(i4) for axi in ax.flat: axi.label_outer() return fig ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1786462635.232227 librosa-1.0.0/tests/test_dtw.py0000644000175100017510000002525415236640653016204 0ustar00runnerrunner#!/usr/bin/env python # -*- encoding: utf-8 -*- import librosa import numpy as np from scipy.spatial.distance import cdist import pytest @pytest.mark.xfail(raises=librosa.ParameterError) def test_1d_input(): X = np.array([[1], [3], [3], [8], [1]]) Y = np.array([[2], [0], [0], [8], [7], [2]]) librosa.sequence.dtw(X=X, Y=Y) def test_dtw_global(): # Example taken from: # Meinard Mueller, Fundamentals of Music Processing X = np.array([[1, 3, 3, 8, 1]]) Y = np.array([[2, 0, 0, 8, 7, 2]]) gt_D = np.array( [ [1.0, 2.0, 3.0, 10.0, 16.0, 17.0], [2.0, 4.0, 5.0, 8.0, 12.0, 13.0], [3.0, 5.0, 7.0, 10.0, 12.0, 13.0], [9.0, 11.0, 13.0, 7.0, 8.0, 14.0], [10, 10.0, 11.0, 14.0, 13.0, 9.0], ] ) mut_D, _ = librosa.sequence.dtw(X, Y) assert np.array_equal(gt_D, mut_D) # Check that it works without backtracking mut_D2 = librosa.sequence.dtw(X, Y, backtrack=False) assert np.array_equal(mut_D, mut_D2) def test_dtw_global_constrained(): # Example taken from: # Meinard Mueller, Fundamentals of Music Processing X = np.array([[1, 3, 3, 8, 1]]) Y = np.array([[2, 0, 0, 8, 7, 2]]) # With band_rad = 0.5, the GT distance array is gt_D = np.array( [ [1.0, 2.0, 3.0, np.inf, np.inf, np.inf], [2.0, 4.0, 5.0, 8.0, np.inf, np.inf], [np.inf, 5.0, 7.0, 10.0, 12.0, np.inf], [np.inf, np.inf, 13.0, 7.0, 8.0, 14.0], [np.inf, np.inf, np.inf, 14.0, 13.0, 9.0], ] ) mut_D = librosa.sequence.dtw( X, Y, backtrack=False, global_constraints=True, band_rad=0.5 ) assert np.array_equal(gt_D, mut_D) def test_dtw_global_supplied_distance_matrix(): # Example taken from: # Meinard Mueller, Fundamentals of Music Processing X = np.array([[1, 3, 3, 8, 1]]) Y = np.array([[2, 0, 0, 8, 7, 2]]) # Precompute distance matrix. C = cdist(X.T, Y.T, metric="euclidean") gt_D = np.array( [ [1.0, 2.0, 3.0, 10.0, 16.0, 17.0], [2.0, 4.0, 5.0, 8.0, 12.0, 13.0], [3.0, 5.0, 7.0, 10.0, 12.0, 13.0], [9.0, 11.0, 13.0, 7.0, 8.0, 14.0], [10, 10.0, 11.0, 14.0, 13.0, 9.0], ] ) # Supply precomputed distance matrix and specify an invalid distance # metric to verify that it isn't used. mut_D, _ = librosa.sequence.dtw(C=C, metric="invalid") assert np.array_equal(gt_D, mut_D) def test_dtw_gobal_boundary(): # Verify that boundary condition is fulfilled for subseq=False. # See https://github.com/librosa/librosa/pull/920 X = np.array([1, 2, 3, 4, 5]) Y = np.array([1, 1, 1, 2, 4, 5, 6, 5, 5]) gt_wp = np.array( [[0, 0], [0, 1], [0, 2], [1, 3], [2, 3], [3, 4], [4, 5], [4, 6], [4, 7], [4, 8]] ) D, wp = librosa.sequence.dtw(X, Y, subseq=False) wp = wp[::-1] assert np.array_equal(gt_wp, wp) def test_dtw_subseq_boundary(): # Verify that boundary condition doesn't have to be fulfilled for # subseq=True. # See https://github.com/librosa/librosa/pull/920 X = np.array([1, 2, 3, 4, 5]) Y = np.array([1, 1, 1, 2, 4, 5, 6, 5, 5]) gt_wp = np.array([[0, 2], [1, 3], [2, 3], [3, 4], [4, 5]]) D, wp = librosa.sequence.dtw(X, Y, subseq=True) wp = wp[::-1] assert np.array_equal(gt_wp, wp) def test_dtw_subseq_steps(): # Verify that the same warping path is computed for backtracking # within the dtw function and manually outside by the user # See https://github.com/librosa/librosa/pull/1166 X = np.array([1, 2, 3, 4, 5]) Y = np.array([1, 1, 1, 2, 4, 5, 6, 5, 5]) gt_wp = np.array([[0, 2], [1, 3], [2, 3], [3, 4], [4, 5]]) D1, wp1 = librosa.sequence.dtw(X, Y, subseq=True, backtrack=True) wp1 = wp1[::-1] D2, steps = librosa.sequence.dtw( X, Y, subseq=True, backtrack=False, return_steps=True ) start_idx = np.argmin(D2[-1, :]) wp2 = librosa.sequence.dtw_backtracking(steps, subseq=True, start=start_idx) wp2 = wp2[::-1] assert np.array_equal(D1, D2) assert np.array_equal(gt_wp, wp1) assert np.array_equal(wp1, wp2) @pytest.mark.xfail(raises=librosa.ParameterError) def test_dtw_backtracking_incompatible_args_01(): # See https://github.com/librosa/librosa/pull/1166 X = np.array([1, 2, 3, 4, 5]) Y = np.array([1, 1, 1, 2, 4, 5, 6, 5, 5]) D, steps = librosa.sequence.dtw( X, Y, subseq=True, backtrack=False, return_steps=True ) start_idx = np.argmin(D[-1, :]) librosa.sequence.dtw_backtracking(steps, subseq=False, start=start_idx) @pytest.mark.xfail(raises=librosa.ParameterError) def test_dtw_incompatible_args_01(): librosa.sequence.dtw(C=1, X=1, Y=1) # type: ignore @pytest.mark.xfail(raises=librosa.ParameterError) def test_dtw_incompatible_args_02(): librosa.sequence.dtw(C=None, X=None, Y=None) # type: ignore @pytest.mark.xfail(raises=librosa.ParameterError) def test_dtw_incompatible_sigma_add(): X = np.array([[1, 3, 3, 8, 1]]) Y = np.array([[2, 0, 0, 8, 7, 2]]) librosa.sequence.dtw(X=X, Y=Y, weights_add=np.arange(10)) @pytest.mark.xfail(raises=librosa.ParameterError) def test_dtw_incompatible_sigma_mul(): X = np.array([[1, 3, 3, 8, 1]]) Y = np.array([[2, 0, 0, 8, 7, 2]]) librosa.sequence.dtw(X=X, Y=Y, weights_mul=np.arange(10)) @pytest.mark.xfail(raises=librosa.ParameterError) def test_dtw_incompatible_sigma_diag(): X = np.array([[1, 3, 3, 8, 1, 2]]) Y = np.array([[2, 0, 0, 8, 7]]) librosa.sequence.dtw(X=X, Y=Y, step_sizes_sigma=np.ones((1, 2), dtype=int)) @pytest.mark.xfail(raises=librosa.ParameterError) def test_dtw_incompatible_sigma_diag_precomp(): C = np.ones((5, 3)) librosa.sequence.dtw(C=C, step_sizes_sigma=[[1, 1]]) # type: ignore def test_dtw_global_diagonal(): # query is a linear ramp X = np.linspace(0.1, 1, 10) Y = X gt_wp = list(zip(list(range(10)), list(range(10))))[::-1] mut_D, mut_wp = librosa.sequence.dtw( X, Y, subseq=True, metric="cosine", step_sizes_sigma=np.array([[1, 1]]), weights_mul=np.array([1]), ) assert np.array_equal(np.asarray(gt_wp), np.asarray(mut_wp)) def test_dtw_subseq(rng): # query is a linear ramp X = np.linspace(0, 1, 100) # database is query surrounded by noise noise_len = 200 noise = rng.random(noise_len) Y = np.concatenate((noise, noise, X, noise)) _, mut_wp = librosa.sequence.dtw(X, Y, subseq=True) # estimated sequence has to match original sequence # note the +1 due to python indexing mut_X = Y[mut_wp[-1][1] : mut_wp[0][1] + 1] assert np.array_equal(X, mut_X) def test_dtw_subseq_supplied_distance_matrix(): X = np.array([[0], [1], [2]]) Y = np.array([[1], [2], [3], [4]]) C = cdist(X, Y) costs0, path0 = librosa.sequence.dtw(X.T, Y.T, subseq=True) costs1, path1 = librosa.sequence.dtw(C=C, subseq=True) assert np.array_equal(costs0, costs1) assert np.array_equal(path0, path1) def test_dtw_subseq_sym(): Y = np.array([10.0, 10.0, 0.0, 1.0, 2.0, 3.0, 10.0, 10.0]) X = np.arange(4) gt_wp_XY = np.array([[3, 5], [2, 4], [1, 3], [0, 2]]) gt_wp_YX = np.array([[5, 3], [4, 2], [3, 1], [2, 0]]) _, mut_wp_XY = librosa.sequence.dtw(X, Y, subseq=True) _, mut_wp_YX = librosa.sequence.dtw(Y, X, subseq=True) assert np.array_equal(gt_wp_XY, mut_wp_XY) assert np.array_equal(gt_wp_YX, mut_wp_YX) def test_dtw_global_constraint_destructive(): # Issue #1029, dtw with global constraints inserts nans # into the cost matrix. This is fine when the cost is computed # locally, but if passed by reference, it's destructive. # This test checks that the cost matrix is unmodified. C1 = np.ones((20, 20)) C2 = np.copy(C1) librosa.sequence.dtw(C=C1, global_constraints=True) assert np.array_equal(C1, C2) def test_dtw_global_inf(): # What should we do if backtracking fails in full sequence mode? # This will happen if the inner loop of bt aborts prematurely # by walking off the edge of the cost array instead of # path-following to (0, 0) # Construct a cost matrix where full alignment is impossible C = np.zeros((4, 4), dtype=float) C[-1, -1] = np.inf with pytest.raises(librosa.ParameterError): librosa.sequence.dtw(C=C, subseq=False) def test_dtw_subseq_inf(): # Construct a cost matrix where partial alignment is impossible C = np.zeros((4, 4), dtype=float) C[-1, :] = np.inf with pytest.raises(librosa.ParameterError): librosa.sequence.dtw(C=C, subseq=True) def test_dtw_subseq_pass(): # Construct a cost matrix where partial alignment is possible C = np.zeros((4, 4), dtype=float) C[-1, 2:] = np.inf librosa.sequence.dtw(C=C, subseq=True) @pytest.mark.xfail(raises=librosa.ParameterError) def test_dtw_nan_fail(): C = np.ones((10, 10)) C[4, 6] = np.nan librosa.sequence.dtw(C=C) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize( "steps", [np.array([[1, -1]]), np.array([[-1, 1]]), np.array([[-1, -1]])] ) def test_dtw_negative_steps(steps): C = np.ones((10, 10)) librosa.sequence.dtw(C=C, step_sizes_sigma=steps) def test_dtw_multi(rng): X = rng.standard_normal(size=(2, 5, 10)) Y = rng.standard_normal(size=(2, 5, 20)) D, wp, steps = librosa.sequence.dtw(X=X, Y=Y, backtrack=True, return_steps=True) # Should give identical results to calling with concatenated inputs Xf = np.concatenate([X[0], X[1]], axis=0) Yf = np.concatenate([Y[0], Y[1]], axis=0) Df, wpf, stepsf = librosa.sequence.dtw( X=Xf, Y=Yf, backtrack=True, return_steps=True ) assert np.allclose(D, Df) assert np.allclose(wp, wpf) assert np.allclose(steps, stepsf) @pytest.mark.parametrize("subseq", [True, False]) def test_path_to_steps_inv(subseq): '''Verify that path_to_steps with inverse does the right thing.''' x1 = np.arange(10) x2 = np.arange(3, 6) cost, path = librosa.sequence.dtw(x1, x2, subseq=subseq) steps1 = librosa.sequence.path_to_steps(path, inverse=False) steps2 = librosa.sequence.path_to_steps(path[:, ::-1], inverse=True) assert np.array_equal(steps1, steps2) @pytest.mark.parametrize("subseq", [True, False]) def test_path_to_steps(subseq): # Some simple test cases to check path_to_steps # We toggle subseq here to ensure that bounds are handled correctly x1 = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) x2 = np.array([1, 4, 7]) cost, path = librosa.sequence.dtw(x1, x2, subseq=subseq) steps = librosa.sequence.path_to_steps(path, inverse=False) if subseq: assert np.array_equal(steps, [3, 4, 5]) else: assert np.array_equal(steps, [0, 3, 6]) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1786462635.232227 librosa-1.0.0/tests/test_effects.py0000644000175100017510000002662215236640653017025 0ustar00runnerrunner#!/usr/bin/env python # -*- encoding: utf-8 -*- """Unit tests for the effects module""" import warnings # Disable cache import os try: os.environ.pop("LIBROSA_CACHE_DIR") except KeyError: pass from contextlib import nullcontext as dnr import numpy as np import pytest import librosa @pytest.fixture(scope="module") def y_multi(): sr = 44100 y = librosa.chirp(fmin=100, fmax=2000, sr=sr, duration=5.0) y = np.vstack([y, y[::-1]]) return y, sr @pytest.fixture(scope="module", params=[22050, 44100]) def ysr(request): sr = request.param y = librosa.chirp(fmin=100, fmax=2000, sr=sr, duration=5.0) return y, sr @pytest.mark.parametrize( "rate,ctx", [ (0.25, dnr()), (0.25, dnr()), (1.0, dnr()), (2.0, dnr()), (4.0, dnr()), (-1, pytest.raises(librosa.ParameterError)), (0, pytest.raises(librosa.ParameterError)), ], ) @pytest.mark.parametrize("n_fft", [2048, 2049]) def test_time_stretch(ysr, rate, ctx, n_fft): with ctx: y, sr = ysr ys = librosa.effects.time_stretch(y, rate=rate, n_fft=n_fft) orig_duration = librosa.get_duration(y=y, sr=sr) new_duration = librosa.get_duration(y=ys, sr=sr) # We don't have to be too precise here, since this goes through an STFT assert np.allclose(orig_duration, rate * new_duration, rtol=1e-2, atol=1e-3) def test_time_stretch_multi(y_multi): y, sr = y_multi # compare each channel C0 = librosa.effects.time_stretch(y[0], rate=1.1) C1 = librosa.effects.time_stretch(y[1], rate=1.1) Call = librosa.effects.time_stretch(y, rate=1.1) # Check each channel assert np.allclose(C0, Call[0]) assert np.allclose(C1, Call[1]) # Verify that they're not all the same assert not np.allclose(Call[0], Call[1]) @pytest.mark.parametrize("n_steps", [-1.5, 1.5, 5]) @pytest.mark.parametrize( "bins_per_octave,ctx", [ (12, dnr()), (24, dnr()), (-1, pytest.raises(librosa.ParameterError)), (0, pytest.raises(librosa.ParameterError)), ], ) @pytest.mark.parametrize("n_fft", [2048, 2049]) def test_pitch_shift(ysr, n_steps, bins_per_octave, ctx, n_fft): with ctx: y, sr = ysr ys = librosa.effects.pitch_shift( y, sr=sr, n_steps=n_steps, bins_per_octave=bins_per_octave, n_fft=n_fft ) orig_duration = librosa.get_duration(y=y, sr=sr) new_duration = librosa.get_duration(y=ys, sr=sr) # We don't have to be too precise here, since this goes through an STFT assert orig_duration == new_duration def test_pitch_shift_multi(y_multi): y, sr = y_multi # compare each channel C0 = librosa.effects.pitch_shift(y[0], sr=sr, n_steps=1) C1 = librosa.effects.pitch_shift(y[1], sr=sr, n_steps=1) Call = librosa.effects.pitch_shift(y, sr=sr, n_steps=1) # Check each channel # Relaxing precision here due to architecture sensitivities on linux-arm64 assert np.allclose(C0, Call[0], atol=1e-6, rtol=1e-6) assert np.allclose(C1, Call[1], atol=1e-6, rtol=1e-6) # Verify that they're not all the same assert not np.allclose(Call[0], Call[1]) @pytest.mark.parametrize("align_zeros", [False, True]) def test_remix_mono(align_zeros): # without zc alignment y = np.asarray([1, 1, -1, -1, 2, 2, -1, -1, 1, 1], dtype=float) y_t = np.asarray([-1, -1, -1, -1, 1, 1, 1, 1, 2, 2], dtype=float) intervals = np.asarray([[2, 4], [6, 8], [0, 2], [8, 10], [4, 6]]) y_out = librosa.effects.remix(y, intervals, align_zeros=align_zeros) assert np.allclose(y_out, y_t) @pytest.mark.parametrize("align_zeros", [False, True]) def test_remix_stereo(align_zeros): # without zc alignment y = np.asarray([1, 1, -1, -1, 2, 2, -1, -1, 1, 1], dtype=float) y_t = np.asarray([-1, -1, -1, -1, 1, 1, 1, 1, 2, 2], dtype=float) y = np.vstack([y, y]) y_t = np.vstack([y_t, y_t]) intervals = np.asarray([[2, 4], [6, 8], [0, 2], [8, 10], [4, 6]]) y_out = librosa.effects.remix(y, intervals, align_zeros=align_zeros) assert np.allclose(y_out, y_t) def test_hpss(ysr): y, sr = ysr y_harm, y_perc = librosa.effects.hpss(y) # Make sure that the residual energy is generally small y_residual = y - y_harm - y_perc rms_orig = librosa.feature.rms(y=y) rms_res = librosa.feature.rms(y=y_residual) assert np.percentile(rms_orig, 0.01) > np.percentile(rms_res, 0.99) def test_hpss_multi(y_multi): y, sr = y_multi # compare each channel CH0, CP0 = librosa.effects.hpss(y[0]) CH1, CP1 = librosa.effects.hpss(y[1]) CHall, CPall = librosa.effects.hpss(y) # Check each channel assert np.allclose(CH0, CHall[0]) assert np.allclose(CP0, CPall[0]) assert np.allclose(CH1, CHall[1]) assert np.allclose(CP1, CPall[1]) # Verify that they're not all the same assert not np.allclose(CHall[0], CHall[1]) assert not np.allclose(CPall[0], CPall[1]) def test_percussive(ysr): y, sr = ysr yh1, yp1 = librosa.effects.hpss(y) yp2 = librosa.effects.percussive(y) assert np.allclose(yp1, yp2) def test_harmonic(ysr): y, sr = ysr yh1, yp1 = librosa.effects.hpss(y) yh2 = librosa.effects.harmonic(y) assert np.allclose(yh1, yh2) @pytest.fixture(scope="module", params=[False, True], ids=["mono", "stereo"]) def y_trim(request): # construct 5 seconds of stereo silence # Stick a sine wave in the middle three seconds sr = 22050 trim_duration = 3.0 y = np.sin(2 * np.pi * 440.0 * np.arange(0, trim_duration * sr) / sr) y = librosa.util.pad_center(y, size=5 * sr) if request.param: y = np.vstack([y, np.zeros_like(y)]) return y @pytest.mark.parametrize("top_db", [60, 40, 20]) @pytest.mark.parametrize("ref", [1, np.max]) @pytest.mark.parametrize("trim_duration", [3.0]) def test_trim(y_trim, top_db, ref, trim_duration): yt, idx = librosa.effects.trim(y_trim, top_db=top_db, ref=ref) # Test for index position fidx = [slice(None)] * y_trim.ndim fidx[-1] = slice(*idx.tolist()) assert np.allclose(yt, y_trim[tuple(fidx)]) # Verify logamp rms = librosa.feature.rms(y=librosa.to_mono(yt), center=False) logamp = librosa.power_to_db(rms**2, ref=ref, top_db=None) assert np.all(logamp > -top_db) # Verify logamp rms_all = librosa.feature.rms(y=librosa.to_mono(y_trim)).squeeze() logamp_all = librosa.power_to_db(rms_all**2, ref=ref, top_db=None) start = int(librosa.samples_to_frames(idx[0])) stop = int(librosa.samples_to_frames(idx[1])) assert np.all(logamp_all[:start] <= -top_db) assert np.all(logamp_all[stop:] <= -top_db) # Verify duration duration = librosa.get_duration(y=yt) assert np.allclose(duration, trim_duration, atol=1e-1), duration def test_trim_empty(): y = np.zeros(1) yt, idx = librosa.effects.trim(y, ref=1) assert yt.size == 0 assert idx[0] == 0 assert idx[1] == 0 def test_trim_multi(y_multi): # Test for https://github.com/librosa/librosa/issues/1489 y, sr = y_multi librosa.effects.trim(y=y) def test_split_multi(y_multi): # Test for https://github.com/librosa/librosa/issues/1489 y, sr = y_multi librosa.effects.split(y=y) @pytest.fixture( scope="module", params=[0, 1, 2, 3], ids=["constant", "end-silent", "full-signal", "gaps"], ) def y_split_idx(request): sr = 8192 y = np.ones(5 * sr) if request.param == 0: # Constant idx_true = np.asarray([[0, 5 * sr]]) elif request.param == 1: # end-silent y[::2] *= -1 y[4 * sr :] = 0 idx_true = np.asarray([[0, 4 * sr]]) elif request.param == 2: # begin-silent y[::2] *= -1 idx_true = np.asarray([[0, 5 * sr]]) else: # begin and end are silent y[::2] *= -1 # Zero out all but two intervals y[:sr] = 0 y[2 * sr : 3 * sr] = 0 y[4 * sr :] = 0 # The true non-silent intervals idx_true = np.asarray([[sr, 2 * sr], [3 * sr, 4 * sr]]) return y, idx_true @pytest.mark.parametrize("frame_length", [1024, 2048, 4096]) @pytest.mark.parametrize("hop_length", [256, 512, 1024]) @pytest.mark.parametrize("top_db", [20, 60, 80]) def test_split(y_split_idx, frame_length, hop_length, top_db): y, idx_true = y_split_idx intervals = librosa.effects.split( y, top_db=top_db, frame_length=frame_length, hop_length=hop_length ) assert np.all(intervals <= y.shape[-1]) int_match = librosa.util.match_intervals(intervals, idx_true) for i in range(len(intervals)): i_true = idx_true[int_match[i]] assert np.all(np.abs(i_true - intervals[i]) <= frame_length), intervals[i] @pytest.mark.parametrize("coef", [0.5, 0.99]) @pytest.mark.parametrize("zi", [None, 0, [0]]) @pytest.mark.parametrize("return_zf", [False, True]) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) def test_preemphasis(coef, zi, return_zf: bool, dtype): x = np.arange(10, dtype=dtype) if return_zf: y, zf = librosa.effects.preemphasis(x, coef=coef, zi=zi, return_zf=return_zf) else: y = librosa.effects.preemphasis(x, coef=coef, zi=zi, return_zf=return_zf) assert np.allclose(y[1:], x[1:] - coef * x[:-1]) assert x.dtype == y.dtype @pytest.mark.parametrize("dtype", [np.float32, np.float64]) def test_preemphasis_continue(dtype): # Compare pre-emphasis computed in parts to that of the whole sequence in one go x = np.arange(64, dtype=dtype) y1, zf1 = librosa.effects.preemphasis(x[:32], return_zf=True) y2, zf2 = librosa.effects.preemphasis(x[32:], return_zf=True, zi=zf1) y_all, zf_all = librosa.effects.preemphasis(x, return_zf=True) assert np.allclose(y_all, np.concatenate([y1, y2])) assert np.allclose(zf2, zf_all) assert x.dtype == y_all.dtype def test_preemphasis_multi(y_multi): y, sr = y_multi # compare each channel C0, zf0 = librosa.effects.preemphasis(y[0], return_zf=True) C1, zf1 = librosa.effects.preemphasis(y[1], return_zf=True) Call, zf = librosa.effects.preemphasis(y, return_zf=True) # Check each channel assert np.allclose(C0, Call[0]) assert np.allclose(C1, Call[1]) assert np.allclose(zf0, zf[0]) assert np.allclose(zf1, zf[1]) # Verify that they're not all the same assert not np.allclose(Call[0], Call[1]) assert not np.allclose(zf[0], zf[1]) def test_deemphasis_multi(y_multi): y, sr = y_multi # compare each channel C0, zf0 = librosa.effects.deemphasis(y[0], return_zf=True) C1, zf1 = librosa.effects.deemphasis(y[1], return_zf=True) Call, zf = librosa.effects.deemphasis(y, return_zf=True) # Check each channel assert np.allclose(C0, Call[0]) assert np.allclose(C1, Call[1]) assert np.allclose(zf0, zf[0]) assert np.allclose(zf1, zf[1]) # Verify that they're not all the same assert not np.allclose(Call[0], Call[1]) assert not np.allclose(zf[0], zf[1]) @pytest.mark.parametrize("coef", [0.5, 0.99]) @pytest.mark.parametrize("zi", [None, 0, [0]]) @pytest.mark.parametrize("return_zf", [False, True]) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) def test_deemphasis(coef, zi, return_zf, dtype): x = np.arange(10, dtype=dtype) y = librosa.effects.preemphasis(x, coef=coef, zi=zi, return_zf=return_zf) if return_zf: y, zf = y y_deemph = librosa.effects.deemphasis(y, coef=coef, zi=zi) assert np.allclose(x, y_deemph) assert x.dtype == y_deemph.dtype ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1786462635.232227 librosa-1.0.0/tests/test_failures.py0000644000175100017510000000575315236640653017222 0ustar00runnerrunner#!/usr/bin/env python # CREATED:2014-12-29 10:52:23 by Brian McFee # unit tests for ill-formed inputs # Disable cache import os try: os.environ.pop("LIBROSA_CACHE_DIR") except KeyError: pass import numpy as np import librosa import pytest @pytest.mark.xfail(raises=librosa.ParameterError) def test_valid_audio_int(): y = np.zeros(10, dtype=int) librosa.util.valid_audio(y) @pytest.mark.xfail(raises=librosa.ParameterError) def test_valid_audio_scalar(): y = np.array(0.0) librosa.util.valid_audio(y) def test_valid_stereo_or_mono(): """valid_audio: mono=False, y.ndim==1""" y = np.zeros(1000) librosa.util.valid_audio(y) def test_valid_stereo(): """valid_audio: mono=False, y.ndim==2""" y = np.zeros((1000, 2)).T librosa.util.valid_audio(y) @pytest.mark.xfail(raises=librosa.ParameterError) def test_valid_audio_type(): """valid_audio: list input""" y = list(np.zeros(1000)) librosa.util.valid_audio(y) # type: ignore @pytest.mark.xfail(raises=librosa.ParameterError) def test_valid_audio_nan(): """valid_audio: NaN""" y = np.zeros(1000) y[10] = np.nan librosa.util.valid_audio(y) @pytest.mark.xfail(raises=librosa.ParameterError) def test_valid_audio_inf(): """valid_audio: Inf""" y = np.zeros(1000) y[10] = np.inf librosa.util.valid_audio(y) def test_valid_audio_strided(): """valid_audio: strided""" y = np.zeros(1000)[::2] librosa.util.valid_audio(y) def test_valid_audio_clang(): """valid_audio: C-contiguous""" y = np.zeros(1000).reshape(2, 500) librosa.util.valid_audio(y) @pytest.mark.xfail(raises=librosa.ParameterError) def test_frame_hop(): """frame: hop_length=0""" y = np.zeros(128) librosa.util.frame(y, frame_length=10, hop_length=0) @pytest.mark.xfail(raises=librosa.ParameterError) def test_frame_size(): # frame: len(y) == 128, frame_length==256, hop_length=128 y = np.zeros(64) librosa.util.frame(y, frame_length=256, hop_length=128) @pytest.mark.xfail(raises=librosa.ParameterError) def test_frame_size_difference(): # In response to issue #385 # https://github.com/librosa/librosa/issues/385 # frame: len(y) == 129, frame_length==256, hop_length=128 y = np.zeros(129) librosa.util.frame(y, frame_length=256, hop_length=128) @pytest.mark.xfail(raises=librosa.ParameterError) def test_stft_bad_window(): y = np.zeros(22050 * 5) n_fft = 2048 window = np.ones(n_fft // 2) librosa.stft(y, n_fft=n_fft, window=window) @pytest.mark.xfail(raises=librosa.ParameterError) def test_istft_bad_window(): D = np.zeros((1025, 10), dtype=np.complex64) n_fft = 2 * (D.shape[0] - 1) window = np.ones(n_fft // 2) librosa.istft(D, window=window) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("y", [np.empty(22050)]) @pytest.mark.parametrize("mode", ["wrap", "maximum", "minimum", "median", "mean"]) def test_stft_bad_pad(y, mode): librosa.stft(y, pad_mode=mode) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2332273 librosa-1.0.0/tests/test_features.py0000644000175100017510000011312315236640653017215 0ustar00runnerrunner#!/usr/bin/env python # -*- encoding: utf-8 -*- import warnings import numpy as np import scipy import pytest import librosa # Disable cache import os try: os.environ.pop("LIBROSA_CACHE_DIR") except KeyError: pass warnings.resetwarnings() warnings.simplefilter("always") warnings.filterwarnings("module", ".*", FutureWarning, "scipy.*") # utils submodule @pytest.mark.parametrize("slope", np.linspace(-2, 2, num=6)) @pytest.mark.parametrize("xin", [np.vstack([np.arange(100.0)] * 3)]) @pytest.mark.parametrize("order", [1]) @pytest.mark.parametrize("width, axis", [(3, 0), (3, 1), (5, 1), (7, 1)]) @pytest.mark.parametrize("bias", [-10, 0, 10]) def test_delta(xin, width, slope, order, axis, bias): x = slope * xin + bias # Note: this test currently only checks first-order differences # if width < 3 or np.mod(width, 2) != 1 or width > x.shape[axis]: # pytest.raises(librosa.ParameterError) delta = librosa.feature.delta(x, width=width, order=order, axis=axis) # Check that trimming matches the expected shape assert x.shape == delta.shape # Once we're sufficiently far into the signal (ie beyond half_len) # (x + delta)[t] should approximate x[t+1] if x is actually linear slice_orig = [slice(None)] * x.ndim slice_out = [slice(None)] * delta.ndim slice_orig[axis] = slice(width // 2 + 1, -width // 2 + 1) slice_out[axis] = slice(width // 2, -width // 2) assert np.allclose((x + delta)[tuple(slice_out)], x[tuple(slice_orig)]) @pytest.mark.xfail(raises=librosa.ParameterError) def test_delta_badorder(): x = np.ones((10, 10)) librosa.feature.delta(x, order=0) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("x", [np.ones((3, 100))]) @pytest.mark.parametrize( "width, axis", [ (-1, 0), (-1, 1), (0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1), (4, 0), (4, 1), (5, 0), (6, 0), (6, 1), (7, 0), ], ) def test_delta_badwidthaxis(x, width, axis): librosa.feature.delta(x, width=width, axis=axis) @pytest.mark.parametrize("data", [np.arange(5.0), np.remainder(np.arange(10000), 24)]) @pytest.mark.parametrize("delay", [-4, -2, -1, 1, 2, 4]) @pytest.mark.parametrize("n_steps", [1, 2, 3, 300]) def test_stack_memory(data, n_steps, delay): data_stack = librosa.feature.stack_memory(data, n_steps=n_steps, delay=delay) # If we're one-dimensional, reshape for testing if data.ndim == 1: data = data.reshape((1, -1)) d, t = data.shape assert data_stack.shape[0] == n_steps * d assert data_stack.shape[1] == t assert np.allclose(data_stack[0], data[0]) for i in range(d): for step in range(1, n_steps): if delay > 0: assert np.allclose( data[i, : -step * delay], data_stack[step * d + i, step * delay :] ) else: assert np.allclose( data[i, -step * delay :], data_stack[step * d + i, : step * delay] ) assert np.max(data) + 1e-7 >= np.max(data_stack) assert np.min(data) - 1e-7 <= np.min(data_stack) @pytest.mark.parametrize("n_steps,delay", [(0, 1), (-1, 1), (1, 0)]) @pytest.mark.parametrize("data", [np.zeros((2, 2))]) @pytest.mark.xfail(raises=librosa.ParameterError) def test_stack_memory_fail(data, n_steps, delay): librosa.feature.stack_memory(data, n_steps=n_steps, delay=delay) @pytest.mark.parametrize("data", [np.zeros((2, 0))]) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("delay", [-2, -1, 1, 2]) @pytest.mark.parametrize("n_steps", [1, 2]) def test_stack_memory_ndim_badshape(data, delay, n_steps): librosa.feature.stack_memory(data, n_steps=n_steps, delay=delay) @pytest.fixture(scope="module") def S_ideal(): # An idealized spectrum with all zero energy except at one DFT band S = np.zeros((513, 3)) S[5, :] = 1.0 return S # spectral submodule @pytest.mark.parametrize( "freq", [ None, librosa.fft_frequencies(sr=22050, n_fft=1024), 3 * librosa.fft_frequencies(sr=22050, n_fft=1024), np.multiply.outer(np.arange(513), np.arange(1, 4)), ], ) def test_spectral_centroid_synthetic(S_ideal, freq): n_fft = 2 * (S_ideal.shape[0] - 1) cent = librosa.feature.spectral_centroid(S=S_ideal, freq=freq) if freq is None: freq = librosa.fft_frequencies(sr=22050, n_fft=n_fft) assert np.allclose(cent, freq[5]) @pytest.mark.parametrize("S", [-np.ones((9, 3)), -np.ones((9, 3)) * 1.0j]) @pytest.mark.xfail(raises=librosa.ParameterError) def test_spectral_centroid_errors(S): librosa.feature.spectral_centroid(S=S) @pytest.mark.parametrize("sr", [22050]) @pytest.mark.parametrize( "y,S", [(np.zeros(3 * 22050), None), (None, np.zeros((1025, 10)))] ) def test_spectral_centroid_empty(y, sr, S): cent = librosa.feature.spectral_centroid(y=y, sr=sr, S=S) assert not np.any(cent) @pytest.mark.parametrize( "freq", [ None, librosa.fft_frequencies(sr=22050, n_fft=1024), 3 * librosa.fft_frequencies(sr=22050, n_fft=1024), np.multiply.outer(np.arange(513), np.arange(1, 4)), ], ) @pytest.mark.parametrize("norm", [False, True]) @pytest.mark.parametrize("p", [1, 2]) def test_spectral_bandwidth_synthetic(S_ideal, freq, norm, p): # This test ensures that a signal confined to a single frequency bin # always achieves 0 bandwidth bw = librosa.feature.spectral_bandwidth(S=S_ideal, freq=freq, norm=norm, p=p) assert not np.any(bw) @pytest.mark.parametrize( "freq", [ None, librosa.fft_frequencies(sr=22050, n_fft=1024), 3 * librosa.fft_frequencies(sr=22050, n_fft=1024), librosa.fft_frequencies(sr=22050, n_fft=1024)[:, np.newaxis], ], ) def test_spectral_bandwidth_onecol(S_ideal, freq): # This test checks for issue https://github.com/librosa/librosa/issues/552 # failure when the spectrogram has a single column bw = librosa.feature.spectral_bandwidth(S=S_ideal[:, :1], freq=freq) assert bw.shape == (1, 1) def test_spectral_bandwidth_flat(): # Generate a flat spectrum S = np.ones((1025, 1)) # Pre-computed reference values bw1 = librosa.feature.spectral_bandwidth(S=S, p=1) assert np.isclose(bw1, 2758.93902439) bw2 = librosa.feature.spectral_bandwidth(S=S, p=2) assert np.isclose(bw2, 3185.74989294) # If the frequencies are just 0, 1, 2, ..., S.shape[0]-1 # then the bandwidth is std(arange(S.shape[0])) freqs = np.arange(1025) bwf = librosa.feature.spectral_bandwidth(S=S, freq=freqs) assert np.isclose(bwf, np.std(freqs)) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("S", [-np.ones((17, 2)), -np.ones((17, 2)) * 1.0j]) def test_spectral_bandwidth_errors(S): librosa.feature.spectral_bandwidth(S=S) @pytest.mark.parametrize("S", [np.ones((1025, 3))]) @pytest.mark.parametrize( "freq", [ None, librosa.fft_frequencies(sr=22050, n_fft=2048), np.multiply.outer(np.arange(1025), np.arange(1, 4)), ], ) @pytest.mark.parametrize("pct", [0.25, 0.5, 0.95]) def test_spectral_rolloff_synthetic(S, freq, pct): sr = 22050 rolloff = librosa.feature.spectral_rolloff(S=S, sr=sr, freq=freq, roll_percent=pct) n_fft = 2 * (S.shape[0] - 1) if freq is None: freq = librosa.fft_frequencies(sr=sr, n_fft=n_fft) idx = np.floor(pct * freq.shape[0]).astype(int) assert np.allclose(rolloff, freq[idx]) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize( "S,pct", [ (-np.ones((513, 3)), 0.95), (-np.ones((513, 3)) * 1.0j, 0.95), (np.ones((513, 3)), -1), (np.ones((513, 3)), 2), ], ) def test_spectral_rolloff_errors(S, pct): librosa.feature.spectral_rolloff(S=S, roll_percent=pct) @pytest.fixture(scope="module") def y_chirp(): sr = 22050 y = librosa.chirp(fmin=100, fmax=1000, sr=sr, duration=5) return y, sr @pytest.fixture(scope="module") def y_clicks(): sr = 22050 y = librosa.clicks(times=np.arange(0.5, 12, 0.5), sr=sr, length=12 * sr) return y, sr def test_spectral_contrast_log(y_chirp): # We already have a regression test for linear energy difference # This test just does a sanity-check on the log-scaled version y, sr = y_chirp contrast = librosa.feature.spectral_contrast(y=y, sr=sr, linear=False) assert not np.any(contrast < 0) @pytest.mark.parametrize("linear", [False, True]) def test_spectral_contrast_flat(linear): # Generate a flat spectrum S = np.ones((1025, 10)) # Pre-computed reference values contrast = librosa.feature.spectral_contrast(S=S, linear=linear) assert np.allclose(contrast, 0.0) @pytest.mark.parametrize("linear", [False, True]) def test_spectral_contrast_maximum(linear): S = np.zeros((1025, 1)) S[512, 0] = 1.0 # Using an extreme quantile value to force max/min contrast = librosa.feature.spectral_contrast(S=S, linear=linear, quantile=0.001) # Reference values for this synthetic spectrum if linear: assert np.allclose(contrast[:, 0], [0, 0, 0, 0, 0, 1, 0]) else: assert np.allclose(contrast[:, 0], [20, 20, 20, 20, 20, 100, 20]) @pytest.mark.parametrize("S", [np.ones((1025, 10))]) @pytest.mark.parametrize( "freq,fmin,n_bands,quantile", [ (0, 200, 6, 0.02), (np.zeros(1 + 1025), 200, 6, 0.02), (np.zeros((1025, 10)), 200, 6, 0.02), (None, -1, 6, 0.02), (None, 0, 6, 0.02), (None, 200, -1, 0.02), (None, 200, 6, -1), (None, 200, 6, 2), (None, 200, 7, 0.02), ], ) @pytest.mark.xfail(raises=librosa.ParameterError) def test_spectral_contrast_errors(S, freq, fmin, n_bands, quantile): librosa.feature.spectral_contrast( S=S, freq=freq, fmin=fmin, n_bands=n_bands, quantile=quantile ) @pytest.mark.parametrize( "S,flatness_ref", [ (np.array([[1, 3], [2, 1], [1, 2]]), np.array([[0.7937005259, 0.7075558390]])), (np.ones((1025, 2)), np.ones((1, 2))), (np.zeros((1025, 2)), np.ones((1, 2))), ], ) def test_spectral_flatness_synthetic(S, flatness_ref): flatness = librosa.feature.spectral_flatness(S=S) assert np.allclose(flatness, flatness_ref) @pytest.mark.parametrize("S", [np.ones((1025, 2))]) @pytest.mark.parametrize("amin", [0, -1]) @pytest.mark.xfail(raises=librosa.ParameterError) def test_spectral_flatness_errors(S, amin): librosa.feature.spectral_flatness(S=S, amin=amin) @pytest.mark.parametrize("S", [-np.ones((1025, 2)), -np.ones((1025, 2)) * 1.0j]) @pytest.mark.xfail(raises=librosa.ParameterError) def test_spectral_flatness_badtype(S): librosa.feature.spectral_flatness(S=S) @pytest.mark.parametrize("n", range(10, 100, 10)) def test_rms_const(n): S = np.ones((n, 5)) # RMSE of an all-ones band is 1 frame_length = 2 * (n - 1) rms = librosa.feature.rms(S=S, frame_length=frame_length) assert np.allclose(rms, np.ones_like(rms) / np.sqrt(frame_length), atol=1e-2) @pytest.mark.parametrize("frame_length", [2048, 2049, 4096, 4097]) @pytest.mark.parametrize("hop_length", [128, 512, 1024]) @pytest.mark.parametrize("center", [False, True]) def test_rms(y_chirp, frame_length, hop_length, center): y1, sr = y_chirp # Ensure audio is divisible into frame size. y1 = librosa.util.fix_length(y1, size=y1.size - y1.size % frame_length) assert y1.size % frame_length == 0 # STFT magnitudes with a constant windowing function and no centering. S1 = librosa.magphase( librosa.stft( y1, n_fft=frame_length, hop_length=hop_length, window=np.ones, center=center ) )[0] rms1 = librosa.feature.rms(S=S1, frame_length=frame_length, hop_length=hop_length) rms2 = librosa.feature.rms( y=y1, frame_length=frame_length, hop_length=hop_length, center=center ) assert rms1.shape == rms2.shape np.testing.assert_allclose(rms1, rms2, atol=5e-4) @pytest.mark.xfail(raises=librosa.ParameterError) def test_rms_noinput(): librosa.feature.rms(y=None, S=None) @pytest.mark.xfail(raises=librosa.ParameterError) def test_rms_badshape(): S = np.zeros((100, 3)) librosa.feature.rms(S=S, frame_length=100) @pytest.mark.parametrize("bad", [np.nan, np.inf, -np.inf]) @pytest.mark.xfail(raises=librosa.ParameterError) def test_rms_nonfinite(bad): # A single non-finite sample used to propagate silently: NaN compares # False against every threshold, so effects.trim and effects.split # concluded the whole signal was silence and returned nothing. y = np.zeros(4096, dtype=np.float32) y[2048] = bad librosa.feature.rms(y=y) @pytest.mark.parametrize("effect", [librosa.effects.trim, librosa.effects.split]) @pytest.mark.xfail(raises=librosa.ParameterError) def test_effects_nonfinite(effect): # These route through rms, so they inherited the missing check. y = np.zeros(4096, dtype=np.float32) y[2048] = np.nan effect(y) @pytest.fixture(params=[32, 16, 8, 4, 2], scope="module") def y_zcr(request): sr = 16384 period = request.param y = np.ones(sr) y[::period] = -1 rate = 2.0 / period return y, sr, rate @pytest.mark.parametrize("frame_length", [513, 2049]) @pytest.mark.parametrize("hop_length", [128, 256]) @pytest.mark.parametrize("center", [False, True]) def test_zcr_synthetic(y_zcr, frame_length, hop_length, center): y, sr, rate = y_zcr zcr = librosa.feature.zero_crossing_rate( y, frame_length=frame_length, hop_length=hop_length, center=center ) # We don't care too much about the edges if there's padding if center: zcr = zcr[:, frame_length // 2 : -frame_length // 2] # We'll allow 1% relative error assert np.allclose(zcr, rate, rtol=1e-2) @pytest.fixture(scope="module", params=[1, 2]) def poly_order(request): return request.param @pytest.fixture(scope="module") def poly_coeffs(poly_order): return np.atleast_1d(np.arange(1, 1 + poly_order)) @pytest.fixture(scope="module", params=[None, 1, 2, -1, "varying"]) def poly_freq(request, rng_mod): freq = librosa.fft_frequencies() if request.param in (1, 2): return freq**request.param elif request.param == -1: return np.cumsum(np.abs(rng_mod.standard_normal(size=(1 + 2048 // 2))), axis=0) elif request.param == "varying": return np.cumsum( np.abs(rng_mod.standard_normal(size=(1 + 2048 // 2, 5))), axis=0 ) else: return None @pytest.fixture(scope="module") def poly_S(poly_coeffs, poly_freq): if poly_freq is None: poly_freq = librosa.fft_frequencies() S = np.zeros_like(poly_freq) for i, c in enumerate(poly_coeffs): S += c * poly_freq**i return S.reshape((poly_freq.shape[0], -1)) def test_poly_features_synthetic(poly_S, poly_coeffs, poly_freq): sr = 22050 n_fft = 2048 order = poly_coeffs.shape[0] - 1 p = librosa.feature.poly_features( S=poly_S, sr=sr, n_fft=n_fft, order=order, freq=poly_freq ) for i in range(poly_S.shape[-1]): assert np.allclose(poly_coeffs, p[::-1, i].squeeze()) @pytest.mark.xfail(raises=librosa.ParameterError) def test_tonnetz_fail_empty(): librosa.feature.tonnetz(y=None, chroma=None) def test_tonnetz_audio(y_chirp): y, sr = y_chirp tonnetz = librosa.feature.tonnetz(y=y, sr=sr) assert tonnetz.shape[0] == 6 @pytest.mark.xfail(raises=librosa.ParameterError) def test_chroma_cqt_badcombo(y_chirp): y, sr = y_chirp librosa.feature.chroma_cqt(y=y, sr=sr, n_chroma=24, bins_per_octave=36) def test_tonnetz_cqt(y_chirp): y, sr = y_chirp chroma_cqt = librosa.feature.chroma_cqt(y=y, sr=sr, n_chroma=36) tonnetz = librosa.feature.tonnetz(chroma=chroma_cqt, sr=sr) assert tonnetz.shape[1] == chroma_cqt.shape[1] assert tonnetz.shape[0] == 6 def test_tonnetz(): # Idealized chroma for [F#:dim, A:aug, A:dim7] chroma = np.array( [ [1, 0, 1], [0, 1, 0], [0, 0, 0], [0, 0, 1], [0, 0, 0], [0, 1, 0], [1, 0, 1], [0, 0, 0], [0, 0, 0], [1, 1, 1], [0, 0, 0], [0, 0, 0], ], dtype=float, ) tonnetz = librosa.feature.tonnetz(chroma=chroma) # F#:dim is [0, 6, 9] # --> fifths space (1/3, 0) # --> min3 space (-1/3, 0) # --> maj3 space (0, 1) # .. but maj3 space is scaled by half in tonnetz assert np.allclose(tonnetz[:, 0], [1 / 3, 0, -1 / 3, 0, 0, 0.5]) # A:aug is [1, 5, 9] # --> origin in fifths space (0, 0) # --> left position in min3 space (-1, 0) # --> origin position in maj3 space (0, 0) assert np.allclose(tonnetz[:, 1], [0, 0, -1, 0, 0, 0]) # A:dim7 is [0, 3, 6, 9] # --> origin in fifths space (0, 0) # --> left position in min3 space (0, 0) # --> right position in maj3 space (1, 0) # .. but maj3 space is scaled by half in tonnetz assert np.allclose(tonnetz[:, 2], [0, 0, 0, 0, 0, 0.5]) @pytest.mark.xfail(raises=librosa.ParameterError) def test_tempogram_fail_noinput(): librosa.feature.tempogram(y=None, onset_envelope=None) @pytest.mark.parametrize("y", [np.zeros(10 * 1000)]) @pytest.mark.parametrize("sr", [1000]) @pytest.mark.parametrize( "win_length,window", [(-384, "hann"), (0, "hann"), (384, np.ones(3))] ) @pytest.mark.xfail(raises=librosa.ParameterError) def test_tempogram_fail_badwin(y, sr, win_length, window): librosa.feature.tempogram(y=y, sr=sr, win_length=win_length, window=window) @pytest.mark.parametrize("hop_length", [512, 1024]) def test_tempogram_audio(y_chirp, hop_length): y, sr = y_chirp oenv = librosa.onset.onset_strength(y=y, sr=sr, hop_length=hop_length) # Get the tempogram from audio t1 = librosa.feature.tempogram( y=y, sr=sr, onset_envelope=None, hop_length=hop_length ) # Get the tempogram from oenv t2 = librosa.feature.tempogram( y=None, sr=sr, onset_envelope=oenv, hop_length=hop_length ) # Make sure it works when both are provided t3 = librosa.feature.tempogram( y=y, sr=sr, onset_envelope=oenv, hop_length=hop_length ) # And that oenv overrides y t4 = librosa.feature.tempogram( y=0 * y, sr=sr, onset_envelope=oenv, hop_length=hop_length ) assert np.allclose(t1, t2) assert np.allclose(t1, t3) assert np.allclose(t1, t4) @pytest.mark.parametrize("tempo", [60, 120, 200]) @pytest.mark.parametrize("center", [False, True]) def test_tempogram_odf_equiv(tempo, center): sr = 22050 hop_length = 512 duration = 8 odf = np.zeros(duration * sr // hop_length) spacing = sr * 60.0 // (hop_length * tempo) odf[:: int(spacing)] = 1 odf_ac = librosa.autocorrelate(odf) tempogram = librosa.feature.tempogram( onset_envelope=odf, sr=sr, hop_length=hop_length, win_length=len(odf), window=np.ones, center=center, norm=None, ) idx = 0 if center: idx = len(odf) // 2 assert np.allclose(odf_ac, tempogram[:, idx]) @pytest.mark.parametrize("tempo", [60, 90, 200]) @pytest.mark.parametrize("win_length", [192, 384]) @pytest.mark.parametrize("window", ["hann", np.ones]) @pytest.mark.parametrize("norm", [None, 1, 2, np.inf]) def test_tempogram_odf_peak(tempo, win_length, window, norm): sr = 22050 hop_length = 512 duration = 8 # Generate an evenly-spaced pulse train odf = np.zeros(duration * sr // hop_length) spacing = sr * 60.0 // (hop_length * tempo) odf[:: int(spacing)] = 1 tempogram = librosa.feature.tempogram( onset_envelope=odf, sr=sr, hop_length=hop_length, win_length=win_length, window=window, norm=norm, ) # Check the shape of the output assert tempogram.shape[0] == win_length assert tempogram.shape[1] == len(odf) # Mean over time to wash over the boundary padding effects idx = np.where(librosa.util.localmax(tempogram.max(axis=1)))[0] # Indices should all be non-zero integer multiples of spacing assert np.allclose(idx, spacing * np.arange(1, 1 + len(idx))) @pytest.mark.parametrize("center", [False, True]) @pytest.mark.parametrize("win_length", [192, 384]) @pytest.mark.parametrize("window", ["hann", np.ones]) @pytest.mark.parametrize("norm", [None, 1, 2, np.inf]) def test_tempogram_odf_multi(center, win_length, window, norm): sr = 22050 hop_length = 512 duration = 8 # Generate an evenly-spaced pulse train odf = np.zeros((10, duration * sr // hop_length)) for i in range(10): spacing = sr * 60.0 // (hop_length * (60 + 12 * i)) odf[i, :: int(spacing)] = 1 tempogram = librosa.feature.tempogram( onset_envelope=odf, sr=sr, hop_length=hop_length, win_length=win_length, window=window, norm=norm, ) for i in range(10): tg_local = librosa.feature.tempogram( onset_envelope=odf[i], sr=sr, hop_length=hop_length, win_length=win_length, window=window, norm=norm, ) assert np.allclose(tempogram[i], tg_local) @pytest.mark.parametrize("y", [np.zeros(10 * 1000)]) @pytest.mark.parametrize("sr", [1000]) @pytest.mark.parametrize( "win_length,window", [(-384, "hann"), (0, "hann"), (384, np.ones(3))] ) @pytest.mark.xfail(raises=librosa.ParameterError) def test_fourier_tempogram_fail_badwin(y, sr, win_length, window): librosa.feature.fourier_tempogram(y=y, sr=sr, win_length=win_length, window=window) @pytest.mark.xfail(raises=librosa.ParameterError) def test_fourier_tempogram_fail_noinput(): librosa.feature.fourier_tempogram(y=None, onset_envelope=None) @pytest.mark.parametrize("hop_length", [512, 1024]) @pytest.mark.filterwarnings( "ignore:n_fft=.*is too large" ) # our test signal is short, but this is fine here def test_fourier_tempogram_audio(y_chirp, hop_length): y, sr = y_chirp oenv = librosa.onset.onset_strength(y=y, sr=sr, hop_length=hop_length) # Get the tempogram from audio t1 = librosa.feature.fourier_tempogram( y=y, sr=sr, onset_envelope=None, hop_length=hop_length ) # Get the tempogram from oenv t2 = librosa.feature.fourier_tempogram( y=None, sr=sr, onset_envelope=oenv, hop_length=hop_length ) # Make sure it works when both are provided t3 = librosa.feature.fourier_tempogram( y=y, sr=sr, onset_envelope=oenv, hop_length=hop_length ) # And that oenv overrides y t4 = librosa.feature.fourier_tempogram( y=0 * y, sr=sr, onset_envelope=oenv, hop_length=hop_length ) assert np.iscomplexobj(t1) assert np.allclose(t1, t2) assert np.allclose(t1, t3) assert np.allclose(t1, t4) @pytest.mark.parametrize("sr", [22050]) @pytest.mark.parametrize("hop_length", [512]) @pytest.mark.parametrize("win_length", [192, 384]) @pytest.mark.parametrize("center", [False, True]) @pytest.mark.parametrize("window", ["hann", np.ones]) def test_fourier_tempogram_invert(sr, hop_length, win_length, center, window): duration = 16 tempo = 100 odf = np.zeros(duration * sr // hop_length, dtype=np.float32) spacing = sr * 60.0 // (hop_length * tempo) odf[:: int(spacing)] = 1 tempogram = librosa.feature.fourier_tempogram( onset_envelope=odf, sr=sr, hop_length=hop_length, win_length=win_length, window=window, center=center, ) if center: sl = slice(None) else: sl = slice(win_length // 2, -win_length // 2) odf_inv = librosa.istft( tempogram, hop_length=1, center=center, window=window, length=len(odf) ) assert np.allclose(odf_inv[sl], odf[sl], atol=1e-6) @pytest.fixture(scope="module") def cens_cqt(): # A synthetic CQT matrix with the following properties # - 12 bins per octave # - 4 octaves # - Each frame adds one more note # - 1 and 2 notes: full energy in cens # - 2 and 3 notes: 75% # - 3-5 notes: 50% # - > 5 notes: 25% # We'll start with one octave and tile it C = np.tri(12, M=8, k=-4)[::-1, ::-1] C[:, -1] = 1 C = np.tile(C, (4, 1)) return C def test_cens_nosmooth_nonorm(cens_cqt): cens = librosa.feature.chroma_cens( C=cens_cqt, win_len_smooth=None, norm=None, bins_per_octave=12 ) # Last frame is uniform, so should be all 0.25 assert np.allclose(cens[:, -1], 0.25) # Value sequence for this data: values = [1, 1, 0.75, 0.75, 0.5, 0.5, 0.5] # Check the quantization for 1-7 notes in chroma for i in range(cens.shape[-1] - 1): assert np.allclose(cens[: i + 1, i], values[i]) assert np.allclose(cens[i + 1 :, i], 0.0) def test_cens_nosmooth(cens_cqt): cens = librosa.feature.chroma_cens( C=cens_cqt, win_len_smooth=None, norm=None, bins_per_octave=12 ) cens1 = librosa.feature.chroma_cens( C=cens_cqt, win_len_smooth=None, norm=1, bins_per_octave=12 ) cens2 = librosa.feature.chroma_cens( C=cens_cqt, win_len_smooth=None, norm=2, bins_per_octave=12 ) assert np.allclose(librosa.util.normalize(cens, norm=1), cens1) assert np.allclose(librosa.util.normalize(cens, norm=2), cens2) def test_cens_nonorm(cens_cqt): cens = librosa.feature.chroma_cens( C=cens_cqt, win_len_smooth=None, norm=None, bins_per_octave=12 ) cens_s = librosa.feature.chroma_cens( C=cens_cqt, win_len_smooth=3, norm=None, bins_per_octave=12 ) # We add 2 on the window to match cens filter = librosa.filters.get_window("hann", 3 + 2, fftbins=False)[np.newaxis, :] # Ensure that filter is normalized to sum to 1 filter /= filter.sum() assert np.allclose(scipy.signal.convolve(cens, filter, mode="same"), cens_s) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("win_len_smooth", [-1, 0, 1.5, "foo"]) def test_cens_fail(y_chirp, win_len_smooth): y, sr = y_chirp librosa.feature.chroma_cens(y=y, sr=sr, win_len_smooth=win_len_smooth) @pytest.mark.parametrize( "S", [librosa.power_to_db(np.arange(128)[:, np.newaxis] ** 2, ref=np.max)] ) @pytest.mark.parametrize("dct_type", [1, 2, 3]) @pytest.mark.parametrize("norm", [None, "ortho"]) @pytest.mark.parametrize("n_mfcc", [13, 20]) @pytest.mark.parametrize("lifter", [0, 13]) def test_mfcc(S, dct_type, norm, n_mfcc, lifter): E_total = np.sum(S, axis=0) mfcc = librosa.feature.mfcc( S=S, dct_type=dct_type, norm=norm, n_mfcc=n_mfcc, lifter=lifter ) assert mfcc.shape[0] == n_mfcc assert mfcc.shape[1] == S.shape[1] # In type-2 mode, DC component should be constant over all frames if dct_type == 2: assert np.var(mfcc[0] / E_total) <= 1e-29 @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("lifter", [-1, np.nan]) def test_mfcc_badlifter(lifter, rng): S = np.ones((128, 100)) librosa.feature.mfcc(S=S, lifter=lifter) # -- feature inversion tests @pytest.mark.parametrize("power", [1, 2]) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) @pytest.mark.parametrize("n_fft", [1024, 2048]) def test_mel_to_stft(power, dtype, n_fft, rng): # Make a random mel spectrum, 4 frames mel_basis = librosa.filters.mel(sr=22050, n_fft=n_fft, n_mels=128, dtype=dtype) stft_orig = rng.standard_normal(size=(n_fft // 2 + 1, 4)) ** power mels = mel_basis.dot(stft_orig.astype(dtype)) stft = librosa.feature.inverse.mel_to_stft(mels, power=power, n_fft=n_fft) # Check precision assert stft.dtype == dtype # Check for non-negative spectrum assert np.all(stft >= 0) # Check that the shape is good assert stft.shape[0] == 1 + n_fft // 2 # Check that the approximation is good in RMSE terms assert np.sqrt(np.mean((mel_basis.dot(stft**power) - mels) ** 2)) <= 5e-2 def test_mel_to_audio(): y = librosa.tone(440.0, sr=22050, duration=1) M = librosa.feature.melspectrogram(y=y, sr=22050) y_inv = librosa.feature.inverse.mel_to_audio(M, sr=22050, length=len(y)) # Sanity check the length assert len(y) == len(y_inv) # And that it's valid audio assert librosa.util.valid_audio(y_inv) @pytest.mark.parametrize("n_mfcc", [13, 20]) @pytest.mark.parametrize("n_mels", [64, 128]) @pytest.mark.parametrize("dct_type", [2, 3]) @pytest.mark.parametrize("lifter", [-1, 0, 1, 2, 3]) @pytest.mark.parametrize("y", [librosa.tone(440.0, sr=22050, duration=1)]) def test_mfcc_to_mel(y, n_mfcc, n_mels, dct_type, lifter): mfcc = librosa.feature.mfcc( y=y, sr=22050, n_mels=n_mels, n_mfcc=n_mfcc, dct_type=dct_type ) # check lifter parameter error if lifter < 0: with pytest.raises(librosa.ParameterError): librosa.feature.inverse.mfcc_to_mel( mfcc * 10**3, n_mels=n_mels, dct_type=dct_type, lifter=lifter ) # check no lifter computations elif lifter == 0: melspec = librosa.feature.melspectrogram(y=y, sr=22050, n_mels=n_mels) mel_recover = librosa.feature.inverse.mfcc_to_mel( mfcc, n_mels=n_mels, dct_type=dct_type ) # Quick shape check assert melspec.shape == mel_recover.shape # Check non-negativity assert np.all(mel_recover >= 0) # check that runtime warnings are triggered when appropriate elif lifter == 2: with pytest.warns((UserWarning, RuntimeWarning)): librosa.feature.inverse.mfcc_to_mel( mfcc * 10**3, n_mels=n_mels, dct_type=dct_type, lifter=lifter ) # check if mfcc_to_mel works correctly with lifter else: ones = np.ones(mfcc.shape, dtype=mfcc.dtype) n_mfcc = mfcc.shape[0] idx = np.arange(1, 1 + n_mfcc, dtype=mfcc.dtype) lifter_sine = 1 + lifter * 0.5 * np.sin(np.pi * idx / lifter)[:, np.newaxis] # compute the recovered mel mel_recover = librosa.feature.inverse.mfcc_to_mel( ones * lifter_sine, n_mels=n_mels, dct_type=dct_type, lifter=lifter ) # compute the expected mel mel_expected = librosa.feature.inverse.mfcc_to_mel( ones, n_mels=n_mels, dct_type=dct_type, lifter=0 ) # assert equality of expected and recovered mels np.testing.assert_almost_equal(mel_recover, mel_expected, 3) @pytest.mark.parametrize("n_mfcc", [13]) @pytest.mark.parametrize("n_mels", [64]) @pytest.mark.parametrize("dct_type", [2, 3]) @pytest.mark.parametrize("lifter", [0, 3]) @pytest.mark.parametrize("y", [librosa.tone(440.0, sr=22050, duration=1)]) def test_mfcc_to_audio(y, n_mfcc, n_mels, dct_type, lifter): mfcc = librosa.feature.mfcc( y=y, sr=22050, n_mels=n_mels, n_mfcc=n_mfcc, dct_type=dct_type ) y_inv = librosa.feature.inverse.mfcc_to_audio( mfcc, n_mels=n_mels, dct_type=dct_type, lifter=lifter, length=len(y) ) # Sanity check the length assert len(y) == len(y_inv) # And that it's valid audio assert librosa.util.valid_audio(y_inv) def test_chroma_vqt_bpo(y_chirp): # Test that bins per octave is properly overridden in chroma y, sr = y_chirp chroma = librosa.feature.chroma_vqt( y=y, sr=sr, intervals=[1, 1.25, 1.5], bins_per_octave=12 ) assert chroma.shape[0] == 3 chroma2 = librosa.feature.chroma_vqt( y=y, sr=sr, intervals="equal", bins_per_octave=12 ) assert chroma2.shape[0] == 12 def test_chroma_vqt_threshold(y_chirp): y, sr = y_chirp c1 = librosa.feature.chroma_vqt(y=y, sr=sr, intervals="pythagorean") c2 = librosa.feature.chroma_vqt(y=y, sr=sr, intervals="pythagorean", threshold=1) # Check that all thresholded points are zero assert np.allclose(c2[c2 < c1], 0) # Check that all non-thresholded points match assert np.all(c2 <= c1) @pytest.mark.xfail(raises=librosa.ParameterError) def test_chroma_vqt_noinput(): librosa.feature.chroma_vqt(y=None, V=None, intervals="ji3") @pytest.mark.xfail(raises=librosa.ParameterError) def test_chroma_cqt_noinput(): librosa.feature.chroma_cqt(y=None, C=None) def test_tempogram_ratio_factors(): # Testing with synthetic data and specific factors # tg is [0, 1, 2, 3, 4] for each frame tg = np.multiply.outer(np.arange(5), np.ones(4)) # frequencies are [1, 2, 4, 8, 16] freqs = 2 ** np.arange(5) factors = np.array([1, 2, 4]) bpm = np.array([4, 2, 1, 1.5]) tgr = librosa.feature.tempogram_ratio(tg=tg, freqs=freqs, factors=factors, bpm=bpm) # frame 0: bpm = 4, factors are [1, 2, 4] => [4, 8, 16] => values 2 3 4 assert np.allclose(tgr[:, 0], [2, 3, 4]) # frame 1: bpm = 2, factors are [1, 2, 4] => [2, 4, 8] => values [0, 2, 3] assert np.allclose(tgr[:, 1], [1, 2, 3]) # frame 2: bpm = 1, factors are [1, 2, 4] => [1, 2, 4] => values [0, 1, 2] assert np.allclose(tgr[:, 2], [0, 1, 2]) # frame 3: bpm = 1.5, factors are [1, 2, 4] => [1.5, 3, 6] => values # [0.5, 1.5, 2.5] assert np.allclose(tgr[:, 3], [0.5, 1.5, 2.5]) @pytest.fixture(scope="module") def tg_ex(y_clicks): y, sr = y_clicks return librosa.feature.tempogram(y=y, sr=sr) def test_tempogram_ratio_aggregate(y_clicks, tg_ex): # Verify that aggregation does its job _, sr = y_clicks tgr1 = librosa.feature.tempogram_ratio(sr=sr, tg=tg_ex, aggregate=None) tgr2 = librosa.feature.tempogram_ratio(sr=sr, tg=tg_ex, aggregate=np.median) assert np.allclose(np.median(tgr1, axis=-1), tgr2) def test_tempogram_ratio_with_tg(y_clicks, tg_ex): # Verify equivalent behavior with/without pre-computed tempogram y, sr = y_clicks tgr1 = librosa.feature.tempogram_ratio(y=y, sr=sr) tgr2 = librosa.feature.tempogram_ratio(tg=tg_ex, sr=sr) assert np.allclose(tgr1, tgr2) def test_tempogram_ratio_with_bpm(y_clicks, tg_ex): y, sr = y_clicks tempo = librosa.feature.tempo(tg=tg_ex, sr=sr, aggregate=None) tgr1 = librosa.feature.tempogram_ratio(tg=tg_ex, sr=sr, bpm=None) tgr2 = librosa.feature.tempogram_ratio(tg=tg_ex, sr=sr, bpm=tempo) assert np.allclose(tgr1, tgr2) def test_metrogram_factors(): # Testing with synthetic data and specific meters factors = np.array([1 / 3, 1 / 4, 1 / 5, 1 / 7]) freqs = np.array([1 / 7, 1 / 5, 1 / 4, 1 / 3, 1]) # each tg frame corresponds to a meter starting from 7/4 tg = np.vstack((np.eye(4), np.ones(4))) met = librosa.feature.metrogram(tg=tg, freqs=freqs, factors=factors) # frame 0: meter = 7/4, factors are [1/3, 1/4, 1/5, 1/7] => values [0 0 0 1] assert np.allclose(met[:, 0], [0, 0, 0, 1]) # frame 1: meter = 5/4, factors are [1/3, 1/4, 1/5, 1/7] => values [0 0 1 0] assert np.allclose(met[:, 1], [0, 0, 1, 0]) # frame 2: meter = 4/4, factors are [1/3, 1/4, 1/5, 1/7] => values [0 1 0 0] assert np.allclose(met[:, 2], [0, 1, 0, 0]) # frame 3: meter = 3/4, factors are [1/3, 1/4, 1/5, 1/7] => values [1 0 0 0] assert np.allclose(met[:, 3], [1, 0, 0, 0]) def test_metrogram_aggregate(y_clicks, tg_ex): # Verify that aggregation does its job _, sr = y_clicks # the first freq is np.inf, we remove it to avoid nans freqs = librosa.tempo_frequencies(sr=sr, n_bins=len(tg_ex))[1:] met1 = librosa.feature.metrogram(tg=tg_ex[1:], freqs=freqs, aggregate=None) met2 = librosa.feature.metrogram(tg=tg_ex[1:], freqs=freqs, aggregate=np.max) assert np.allclose(np.max(met1, axis=-2), met2) def test_hybrid_tempogram(): # Synthesize a short deterministic click track to avoid I/O flakiness sr = 22050 duration = 2.0 n_samples = int(sr * duration) y = np.zeros(n_samples, dtype=float) click_positions = np.arange(0, n_samples, sr // 4) y[click_positions] = 1.0 hop_length = 512 # 1. Basic execution and shape verification htg = librosa.feature.hybrid_tempogram(y=y, sr=sr, hop_length=hop_length) assert htg.ndim == 2, "Output should be a 2D array for mono audio" assert htg.shape[0] > 0, "Frequency dimension should not be empty" assert htg.shape[1] > 0, "Time frames dimension should not be empty" # 2. Value constraints (Geometric mean ensures non-negative results) assert np.all(htg >= 0), "All values in the hybrid tempogram must be non-negative" assert np.all(np.isfinite(htg)), "Output must not contain NaNs or Infs" # 3. Multi-channel (stereo) audio support y_stereo = np.vstack([y, y]) htg_stereo = librosa.feature.hybrid_tempogram(y=y_stereo, sr=sr, hop_length=hop_length) assert htg_stereo.ndim == 3, "Output should be a 3D array for stereo audio" assert htg_stereo.shape[-1] == htg.shape[-1], "Time frames must match between mono and stereo" assert htg_stereo.shape[-2] == htg.shape[-2], "Frequency bins must match between mono and stereo" # 4. Custom parameters (Ensures coverage for kwargs and specific win_length) htg_custom = librosa.feature.hybrid_tempogram( y=y, sr=sr, hop_length=hop_length, win_length=256, kind='linear', fill_value=0.0 ) assert htg_custom.ndim == 2 assert np.all(np.isfinite(htg_custom)), "Custom interpolation should not produce NaNs" # 5. Test with silent audio (all zeros) y_silent = np.zeros_like(y) htg_silent = librosa.feature.hybrid_tempogram(y=y_silent, sr=sr, hop_length=hop_length) assert np.all(htg_silent == 0), "Hybrid tempogram of silence should be all zeros" # 6. Verify frequency grid alignment freqs = librosa.fourier_tempo_frequencies(sr=sr, hop_length=hop_length, win_length=384) assert htg.shape[-2] == len(freqs), "Frequency dimension must match Fourier tempo grid" # 7. Test with arbitrary multi-channel shape (e.g., 5 channels) y_multi = np.tile(y, (5, 1)) htg_multi = librosa.feature.hybrid_tempogram(y=y_multi, sr=sr, hop_length=hop_length) assert htg_multi.shape == (5, len(freqs), htg.shape[-1]), "Multi-channel shape mismatch"././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2332273 librosa-1.0.0/tests/test_filters.py0000644000175100017510000004625115236640653017056 0ustar00runnerrunner#!/usr/bin/env python # CREATED:2013-03-08 15:25:18 by Brian McFee # unit tests for librosa.filters # # This test suite verifies that librosa core routines match (numerically) the output # of various DPWE matlab implementations on a broad range of input parameters. # # All test data is generated by the Matlab script "makeTestData.m". # Each test loads in a .mat file which contains the input and desired output for a given # function. The test then runs the librosa implementation and verifies the results # against the desired output, typically via numpy.allclose(). # # Disable cache import os try: os.environ.pop("LIBROSA_CACHE_DIR") except KeyError: pass from contextlib import nullcontext as dnr import warnings import glob import numpy as np import scipy.io import scipy.signal from typing import Any, ContextManager import pytest import librosa def test_hz_to_mel_htk(): # Test the HTK mel scale freqs = np.array([0, 500, 1000, 2000, 3000]) mels = np.array([0.0, 607.44591966, 999.98553714, 1521.35955416, 1876.45406012]) mel_est = librosa.hz_to_mel(freqs, htk=True) assert np.allclose(mel_est, mels) # Test the scalar version as well for f, m in zip(freqs, mels): m_est = librosa.hz_to_mel(f, htk=True) assert np.isclose(m_est, m) def test_hz_to_mel_sl(): # Test the Slaney mel scale freqs = np.array([0, 500, 1000, 2000, 3000]) mels = np.array([0.0, 7.5, 15.0, 25.08188016, 30.97940199]) mel_est = librosa.hz_to_mel(freqs, htk=False) assert np.allclose(mel_est, mels) # Test the scalar version as well for f, m in zip(freqs, mels): m_est = librosa.hz_to_mel(f, htk=False) assert np.isclose(m_est, m) def test_mel_to_hz_htk(): # Test the HTK mel scale mels = np.array([0.0, 200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800]) freqs = np.array( [ 0.0, 135.92888249, 298.25299511, 492.09787234, 723.58434605, 1000.02181646, 1330.13905319, 1724.35981432, 2195.13198618, 2757.32063694, ] ) freq_est = librosa.mel_to_hz(mels, htk=True) assert np.allclose(freq_est, freqs) for f, m in zip(freqs, mels): f_est = librosa.mel_to_hz(m, htk=True) assert np.isclose(f_est, f) def test_mel_to_hz_sl(): # Test the Slaney mel scale mels = np.array([0, 5, 10, 15, 25, 30]) freqs = np.array( [0.0, 333.33333333, 666.66666667, 1000.0, 1988.77281813, 2804.64413074] ) freq_est = librosa.mel_to_hz(mels, htk=False) assert np.allclose(freq_est, freqs) for f, m in zip(freqs, mels): f_est = librosa.mel_to_hz(m, htk=False) assert np.isclose(f_est, f) def test_hz_to_octs_440(): freqs = np.array([220, 440, 660, 880]) octs = np.array([3, 4, 3 + np.log2(3), 5]) octs_est = librosa.hz_to_octs(freqs) assert np.allclose(octs_est, octs) for f, o in zip(freqs, octs): o_est = librosa.hz_to_octs(f) assert np.isclose(o_est, o) @pytest.mark.parametrize("bpo", [12, 24, 36]) def test_hz_to_octs_detune(bpo): # If we're off by one semitone, then the octave should be # off by 1/bpo oct = librosa.hz_to_octs(440, tuning=-1, bins_per_octave=bpo) assert np.isclose(oct, 4 + 1 / bpo) @pytest.mark.parametrize("sr", [22050]) @pytest.mark.parametrize("n_fft", [1025, 2048]) @pytest.mark.parametrize("n_mels", [40, 128]) @pytest.mark.parametrize("fmin", [0, 30]) @pytest.mark.parametrize("fmax", [None, 8000]) @pytest.mark.parametrize("htk", [False, True]) def test_melfb(sr, n_fft, n_mels, fmin, fmax, htk): melfb = librosa.filters.mel( sr=sr, n_fft=n_fft, n_mels=n_mels, fmin=fmin, fmax=fmax, htk=htk, norm=None, # We'll test normalization separately ) if fmax is None: real_fmax = sr / 2.0 else: real_fmax = fmax # Check the shape assert melfb.shape == (n_mels, 1 + n_fft // 2) # Check non-negativity assert np.all(melfb >= 0) # TODO: check for triangular filter shapes, maybe? # Check that the dominant frequencies are well approximated # by the mel scale # Two caveats here: # 1. the peak may lie in between two DFT bins, so we allow a tolerance proportional to the spacing # 2. because of how the filter bank is constructed, we over-allocate two mel bins and discard the edges # this is inherited from the fft2melmx.m logic here: https://www.ee.columbia.edu/~dpwe/resources/matlab/rastamat/fft2melmx.m real_freqs = librosa.mel_frequencies( fmin=fmin, fmax=real_fmax, n_mels=n_mels + 2, htk=htk )[1:-1] fft_freqs = librosa.fft_frequencies(sr=sr, n_fft=n_fft) peak_freqs = fft_freqs[np.argmax(melfb, axis=1)] bin_width = sr / n_fft assert np.allclose(peak_freqs, real_freqs, atol=bin_width) @pytest.mark.parametrize("norm", [1, 2, np.inf, "slaney"]) def test_mel_norm(norm): sr = 22050 n_fft = 2048 M = librosa.filters.mel(sr=sr, n_fft=n_fft, fmin=30, norm=norm) if norm == 1: assert np.allclose(np.sum(np.abs(M), axis=1), 1) elif norm == 2: assert np.allclose(np.sum(np.abs(M**2), axis=1), 1) elif norm == np.inf: assert np.allclose(np.max(np.abs(M), axis=1), 1) elif norm == "slaney": # Approximately constant-energy per band in Slaney mode # This should be close to norm=1 case for uniformly spaced # input frequencies, but the calculation is in terms of the # center frequencies and not the actual filter coefficients # so there's some approximation error involved. fft_spacing = sr / n_fft # Compute row-sums msum = np.sum(np.abs(M), axis=1) assert np.max(np.abs(msum * fft_spacing - 1)) < 5e-2 @pytest.mark.xfail(raises=librosa.ParameterError) def test_mel_badnorm(): librosa.filters.mel(sr=22050, n_fft=2048, norm="garbage") # type: ignore def test_mel_gap(): # This configuration should trigger some empty filters sr = 44100 n_fft = 1024 fmin = 0 fmax = 2000 n_mels = 128 htk = True with pytest.warns(UserWarning, match="Empty filters"): librosa.filters.mel( sr=sr, n_fft=n_fft, n_mels=n_mels, fmin=fmin, fmax=fmax, htk=htk ) @pytest.mark.parametrize("n_chroma", [12, 24]) @pytest.mark.parametrize("n_fft", [128, 256]) def test_chroma_shape(n_chroma, n_fft): cfb = librosa.filters.chroma(sr=22050, n_fft=n_fft, n_chroma=n_chroma) assert cfb.shape == (n_chroma, n_fft // 2 + 1) @pytest.mark.parametrize("norm", [1, 2, np.inf, None]) def test_chroma_norm(norm): cfbnonorm = librosa.filters.chroma( sr=22050, n_fft=128, n_chroma=12, norm=None, octwidth=None ) cfb = librosa.filters.chroma( sr=22050, n_fft=128, n_chroma=12, norm=norm, octwidth=None ) assert np.allclose(librosa.util.normalize(cfbnonorm, norm=norm, axis=0), cfb) @pytest.mark.parametrize("n_chroma", [12, 24]) def test_chroma_basec(n_chroma): cfb_a = librosa.filters.chroma(sr=22050, n_fft=128, n_chroma=n_chroma, base_c=False) cfb_c = librosa.filters.chroma(sr=22050, n_fft=128, n_chroma=n_chroma, base_c=True) assert np.allclose(np.roll(cfb_a, -3 * (n_chroma // 12), axis=0), cfb_c) @pytest.mark.parametrize("n_chroma", [12, 24]) @pytest.mark.parametrize("tuning", [-1, 1]) def test_chroma_tuning(n_chroma, tuning): # Generate two chroma banks, with tuning off by one semitone cfb_a = librosa.filters.chroma(sr=22050, n_fft=128, n_chroma=n_chroma, tuning=0) cfb_b = librosa.filters.chroma( sr=22050, n_fft=128, n_chroma=n_chroma, tuning=tuning ) # These won't line up identically due to sampling accuracy, but the filters # should be close to a circular shift of each other A = cfb_a.dot(cfb_b.T) # Roll everything back by one semitone to compensate for the tuning change A = np.roll(A, -tuning, axis=0) # The maxima should be along the main diagonal now - check it assert np.allclose(np.argmax(A, axis=0), np.arange(A.shape[0])) @pytest.mark.parametrize("ctroct", [5, 6]) def test_chroma_ctroct(ctroct): cfb = librosa.filters.chroma(sr=22050, n_fft=2048, n_chroma=12, ctroct=ctroct) # Verify that the peak of each channel is in the center octave range peaks = np.argmax(cfb, axis=1) freqs = librosa.fft_frequencies(sr=22050, n_fft=2048) peak_freqs = freqs[peaks] # Convert to octaves peak_octaves = librosa.hz_to_octs(peak_freqs) assert np.allclose(np.round(peak_octaves), ctroct) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) def test_chroma_dtype(dtype): cfb = librosa.filters.chroma(sr=22050, n_fft=2048, n_chroma=12, dtype=dtype) assert cfb.dtype == dtype # Testing two tones, 261.63 Hz and 440 Hz @pytest.mark.parametrize("freq", [261.63, 440]) def test_chroma_issue1295(freq): tone_1 = librosa.tone(frequency=freq, sr=22050, duration=1) chroma_1 = librosa.feature.chroma_stft( y=tone_1, sr=22050, n_chroma=120, base_c=True ) actual_argmax = np.unravel_index(chroma_1.argmax(), chroma_1.shape) if freq == 261.63: assert actual_argmax == (118, 0) # type: ignore[comparison-overlap] elif freq == 440: assert actual_argmax == (90, 0) # type: ignore[comparison-overlap] @pytest.mark.parametrize("n", [16, 16.0, 16.25, 16.75]) @pytest.mark.parametrize( "window_name", [ "barthann", "bartlett", "blackman", "blackmanharris", "bohman", "boxcar", "cosine", "flattop", "hamming", "hann", "nuttall", "parzen", "triang", ], ) def test__window(n, window_name): window = getattr(scipy.signal.windows, window_name) wdec = librosa.filters.__float_window(window) if n == int(n): n = int(n) assert np.allclose(wdec(n), window(n)) else: wf = wdec(n) fn = int(np.floor(n)) assert not np.any(wf[fn:]) @pytest.mark.parametrize("sr", [11025]) @pytest.mark.parametrize("fmin", [librosa.note_to_hz("C3")]) @pytest.mark.parametrize("n_bins", [12, 24]) @pytest.mark.parametrize("bins_per_octave", [12, 24]) @pytest.mark.parametrize("filter_scale", [1, 2]) @pytest.mark.parametrize("norm", [1, 2]) @pytest.mark.parametrize("pad_fft", [False, True]) @pytest.mark.parametrize("gamma", [0, 10, None]) def test_wavelet(sr, fmin, n_bins, bins_per_octave, filter_scale, pad_fft, norm, gamma): freqs = librosa.cqt_frequencies( fmin=fmin, n_bins=n_bins, bins_per_octave=bins_per_octave ) F, lengths = librosa.filters.wavelet( freqs=freqs, sr=sr, filter_scale=filter_scale, pad_fft=pad_fft, norm=norm, gamma=gamma, ) assert np.all(lengths <= F.shape[1]) assert len(F) == n_bins if not pad_fft: return assert np.mod(np.log2(F.shape[1]), 1.0) == 0.0 # Check for vanishing negative frequencies F_fft = np.abs(np.fft.fft(F, axis=1)) # Normalize by row-wise peak F_fft = F_fft / np.max(F_fft, axis=1, keepdims=True) assert np.max(F_fft[:, -F_fft.shape[1] // 2 :]) < 1e-3 @pytest.mark.xfail(raises=librosa.ParameterError) def test_wavelet_lengths_badscale(): librosa.filters.wavelet_lengths(freqs=2 ** np.arange(3), filter_scale=-1) @pytest.mark.xfail(raises=librosa.ParameterError) def test_wavelet_lengths_badgamma(): librosa.filters.wavelet_lengths(freqs=2 ** np.arange(3), gamma=-1) @pytest.mark.xfail(raises=librosa.ParameterError) def test_wavelet_lengths_badfreqs(): librosa.filters.wavelet_lengths(freqs=2 ** np.arange(3) - 20) @pytest.mark.xfail(raises=librosa.ParameterError) def test_wavelet_lengths_badfreqsorder(): librosa.filters.wavelet_lengths(freqs=2 ** np.arange(3)[::-1]) @pytest.mark.xfail(raises=librosa.ParameterError) def test_wavelet_lengths_noalpha(): librosa.filters.wavelet_lengths(freqs=[64], alpha=None) def test_window_bandwidth(): hann_bw = librosa.filters.window_bandwidth("hann") hann_scipy_bw = librosa.filters.window_bandwidth(scipy.signal.windows.hann) assert hann_bw == hann_scipy_bw def test_window_bandwidth_dynamic(): # Test with a window constructor guaranteed to not exist in # the dictionary. # should behave like a box filter, which has enbw == 1 assert librosa.filters.window_bandwidth(lambda n: np.ones(n)) == 1 @pytest.mark.xfail(raises=ValueError) def test_window_bandwidth_missing(): librosa.filters.window_bandwidth("made up window name") def binstr(m): out = [] for row in m: line = [" "] * len(row) for i in np.flatnonzero(row): line[i] = "." out.append("".join(line)) return "\n".join(out) @pytest.mark.parametrize("n_octaves", [2, 3, 4]) @pytest.mark.parametrize("semitones", [1, 3]) @pytest.mark.parametrize("n_chroma", [12, 24, 36]) @pytest.mark.parametrize("fmin", [None] + list(librosa.midi_to_hz(range(48, 61)))) @pytest.mark.parametrize("base_c", [False, True]) @pytest.mark.parametrize("window", [None, [1]]) def test_cq_to_chroma(n_octaves, semitones, n_chroma, fmin, base_c, window): bins_per_octave = 12 * semitones n_bins = n_octaves * bins_per_octave ctx: ContextManager[Any] if np.mod(bins_per_octave, n_chroma) != 0: ctx = pytest.raises(librosa.ParameterError) else: ctx = dnr() with ctx: # Fake up a cqt matrix with the corresponding midi notes if fmin is None: midi_base = 24 # C2 else: midi_base = librosa.hz_to_midi(fmin) midi_notes = np.linspace( midi_base, midi_base + n_bins * 12.0 / bins_per_octave, endpoint=False, num=n_bins, ) # We don't care past 2 decimals here. # the log2 inside hz_to_midi can cause problems though. midi_notes = np.around(midi_notes, decimals=2) C = np.diag(midi_notes) cq2chr = librosa.filters.cq_to_chroma( n_input=C.shape[0], bins_per_octave=bins_per_octave, n_chroma=n_chroma, fmin=fmin, base_c=base_c, window=window, ) chroma = cq2chr.dot(C) for i in range(n_chroma): v = chroma[i][chroma[i] != 0] v = np.around(v, decimals=2) if base_c: resid = np.mod(v, 12) else: resid = np.mod(v - 9, 12) resid = np.round(resid * n_chroma / 12.0) assert np.allclose(np.mod(i - resid, 12), 0.0), i - resid @pytest.mark.xfail(raises=librosa.ParameterError) def test_get_window_fail(): librosa.filters.get_window(None, 32) # type: ignore @pytest.mark.parametrize("window", ["hann", "hann", 4.0, ("kaiser", 4.0)]) def test_get_window(window): w1 = librosa.filters.get_window(window, 32) w2 = scipy.signal.get_window(window, 32) assert np.allclose(w1, w2) def test_get_window_func(): w1 = librosa.filters.get_window(scipy.signal.windows.boxcar, 32) w2 = scipy.signal.get_window("boxcar", 32) assert np.allclose(w1, w2) @pytest.mark.parametrize( "pre_win", [scipy.signal.windows.hann(16), list(scipy.signal.windows.hann(16)), [1, 1, 1]], ) def test_get_window_pre(pre_win): win = librosa.filters.get_window(pre_win, len(pre_win)) assert np.allclose(win, pre_win) def test_semitone_filterbank(): freqs = librosa.midi_to_hz(np.arange(60, 96)) srs = np.array([8000] * len(freqs)) # Only test in SOS mode, we'll do the tf mode compatibility check in a different test sos_fb, sos_srs = librosa.filters.semitone_filterbank( center_freqs=freqs, sample_rates=srs, flayout="sos" ) # Verify that each filter is centered on the corresponding frequency for i in range(len(freqs)): # evaluate the z-transform of the filter # TODO: when we bump scipy to 1.15, we can revise this to the newer name # freqz_sos _, h = scipy.signal.sosfreqz(sos_fb[i], worN=freqs, fs=sos_srs[i]) hmag = np.abs(h) # The filter should have a flat response that's log-symmetric around the center frequency # We'll just check that the peak is not far off from the max (ie due to ripple) assert hmag[i] >= np.max(hmag) * 0.999 @pytest.mark.parametrize("tuning", [-1, 0, 1]) def test_semitone_filterbank_defaults(tuning): sos_fb, sos_srs = librosa.filters.semitone_filterbank(tuning=tuning, flayout="sos") # Verify that each filter is centered on the corresponding frequency freqs = librosa.filters.mr_frequencies(tuning=tuning)[0] for i in range(len(freqs)): # evaluate the z-transform of the filter # TODO: when we bump scipy to 1.15, we can revise this to the newer name # freqz_sos _, h = scipy.signal.sosfreqz(sos_fb[i], worN=freqs, fs=sos_srs[i]) hmag = np.abs(h) # The filter should have a flat response that's log-symmetric around the center frequency # We'll just check that the peak is not far off from the max (ie due to ripple) # Tolerance here is sloppier than the first test above because of variable sampling rates assert hmag[i] >= np.max(hmag) * 0.89 def test_semitone_filterbank_layout(): freqs = librosa.midi_to_hz(np.arange(60, 96)) srs = np.array([8000] * len(freqs)) ba_fb, ba_srs = librosa.filters.semitone_filterbank( center_freqs=freqs, sample_rates=srs, flayout="ba" ) sos_fb, sos_srs = librosa.filters.semitone_filterbank( center_freqs=freqs, sample_rates=srs, flayout="sos" ) assert np.allclose(ba_srs, sos_srs) assert np.allclose(ba_srs, srs) # Verify that the sos layout converts to the ba layout for i in range(len(sos_fb)): tf = scipy.signal.sos2tf(sos_fb[i]) assert np.allclose(tf[0], ba_fb[i][0]) assert np.allclose(tf[1], ba_fb[i][1]) @pytest.mark.parametrize("n", [9, 17]) @pytest.mark.parametrize("window", ["hann", "rect"]) @pytest.mark.parametrize("angle", [None, np.pi / 4, np.pi / 6]) @pytest.mark.parametrize("slope", [1, 2, 0.5]) @pytest.mark.parametrize("zero_mean", [False, True]) def test_diagonal_filter(n, window, angle, slope, zero_mean): kernel = librosa.filters.diagonal_filter( window, n, slope=slope, angle=angle, zero_mean=zero_mean ) # In the no-rotation case, check that the filter is shaped correctly if angle == np.pi / 4 and not zero_mean: win_unnorm = librosa.filters.get_window(window, n, fftbins=False) win_unnorm /= win_unnorm.sum() assert np.allclose(np.diag(kernel), win_unnorm) # First check: zero-mean if zero_mean: assert np.isclose(kernel.sum(), 0) else: assert np.isclose(kernel.sum(), 1) and np.all(kernel >= 0) # Now check if the angle transposes correctly if angle is None: # If we're using the slope API, then the transposed kernel # will have slope 1/slope k2 = librosa.filters.diagonal_filter( window, n, slope=1.0 / slope, angle=angle, zero_mean=zero_mean ) else: # If we're using the angle API, then the transposed kernel # will have angle pi/2 - angle k2 = librosa.filters.diagonal_filter( window, n, slope=slope, angle=np.pi / 2 - angle, zero_mean=zero_mean ) assert np.allclose(k2, kernel.T) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2332273 librosa-1.0.0/tests/test_intervals.py0000644000175100017510000001340515236640653017410 0ustar00runnerrunner#!/usr/bin/env python # -*- encoding: utf-8 -*- # CREATED:2022-08-09 06:34:21 by Brian McFee """Unit tests for just intonation and friends""" import os import sys try: os.environ.pop("LIBROSA_CACHE_DIR") except KeyError: pass import warnings import numpy as np import pytest import librosa def test_pythagorean(): ivals = librosa.pythagorean_intervals(bins_per_octave=6, sort=False) assert np.allclose(ivals, [1, 3 / 2, 9 / 8, 27 / 16, 81 / 64, 243 / 128]) ivals2 = librosa.pythagorean_intervals(bins_per_octave=6, sort=True) assert np.allclose(sorted(ivals), ivals2) def test_plimit3(): intervals = librosa.plimit_intervals(primes=[3], bins_per_octave=24, sort=False) intervals_s = librosa.plimit_intervals(primes=[3], bins_per_octave=24, sort=True) assert np.allclose(sorted(intervals), intervals_s) assert np.allclose( intervals, [ 1, # These are just alternating powers of 3/2 3 / 2, 4 / 3, 9 / 8, 16 / 9, 32 / 27, 27 / 16, 128 / 81, 81 / 64, 256 / 243, 243 / 128, 729 / 512, 2187 / 2048, 6561 / 4096, 1024 / 729, 4096 / 2187, 8192 / 6561, 7625 / 6347, 10037 / 6029, 13654 / 7577, 7577 / 6827, 12457 / 9217, 2006 / 1979, 12497 / 8445, ], ) def test_plimit5(): intervals = librosa.plimit_intervals(primes=[3, 5], bins_per_octave=24, sort=False) intervals_s = librosa.plimit_intervals(primes=[3, 5], bins_per_octave=24, sort=True) assert np.allclose(sorted(intervals), intervals_s) assert np.allclose( intervals, [ 1, # Unison 3 / 2, # P5 4 / 3, # P4 9 / 8, # major wholetone 5 / 4, # Pt M3 15 / 8, # Pt M7 5 / 3, # Pt M6 45 / 32, # Pt TT 8 / 5, # Pt m6 6 / 5, # Pt m3 16 / 15, # major diatonic semitone 9 / 5, # Pt m7 16 / 9, # Py m7 27 / 16, # Py M6 10 / 9, # minor wholetone 64 / 45, # Pt dim5 32 / 27, # Py m3 25 / 16, # Pt aug5 75 / 64, # Pt aug2 25 / 24, # minor chromatic semitone 135 / 128, # major limma 225 / 128, # Pt aug6 40 / 27, # Pt narrow 5 25 / 18, # Rameau's tritone ], ) def test_plimit7(): intervals = librosa.plimit_intervals( primes=[3, 5, 7], bins_per_octave=24, sort=False ) intervals_s = librosa.plimit_intervals( primes=[3, 5, 7], bins_per_octave=24, sort=True ) assert np.allclose(sorted(intervals), intervals_s) assert np.allclose( intervals, [ 1, # Unison 3 / 2, # P5 4 / 3, # P4 9 / 8, # major whole tone 5 / 4, # Pt M3 15 / 8, # Pt M7 5 / 3, # Pt M6 45 / 32, # Pt TT 8 / 5, # Pt m6 6 / 5, # Pt m3 16 / 15, # major diatonic semitone 9 / 5, # Pt m7 16 / 9, # Py m7 27 / 16, # Py M6 7 / 4, # N7 21 / 16, # septimal narrow 4 8 / 7, # septimal whole tone 12 / 7, # septimal M7 9 / 7, # septimal M3 32 / 21, # septimal wide fifth 7 / 6, # septimal m3 63 / 32, # septimal narrow octave 35 / 32, # septimal N2 105 / 64, # septimal N6 ], ) @pytest.mark.parametrize("n_bins", [6, 12, 24, 30]) @pytest.mark.parametrize( "intervals", ["equal", "pythagorean", "ji3", "ji5", "ji7", [1, 4 / 3, 3 / 2, 5 / 4]] ) @pytest.mark.parametrize("bins_per_octave", [6, 12, 15]) def test_interval_frequencies(n_bins, intervals, bins_per_octave): freqs = librosa.interval_frequencies( n_bins, fmin=10, intervals=intervals, bins_per_octave=bins_per_octave ) assert len(freqs) == n_bins assert min(freqs) == 10 @pytest.mark.parametrize( "intervals", ["pythagorean", "ji3", "ji5", "ji7", [1, 3 / 2, 4 / 3, 5 / 4]] ) def test_intervals_sorted(intervals): freqs = librosa.interval_frequencies(12, fmin=1, intervals=intervals, sort=False) freqs_s = librosa.interval_frequencies(12, fmin=1, intervals=intervals, sort=True) assert not np.allclose(freqs, freqs_s) assert np.allclose(sorted(freqs), freqs_s) @pytest.mark.parametrize("sort", [False, True]) def test_pythagorean_factorizations(sort): intervals = librosa.pythagorean_intervals( bins_per_octave=20, sort=sort, return_factors=False ) factors = librosa.pythagorean_intervals( bins_per_octave=20, sort=sort, return_factors=True ) assert len(intervals) == len(factors) for ival, facts in zip(intervals, factors): value = 0.0 for prime in facts: value += facts[prime] * np.log2(prime) assert np.isclose(ival, np.power(2, value)) @pytest.mark.parametrize("sort", [False, True]) @pytest.mark.parametrize("primes", [[3], [3, 5], [3, 5, 7]]) def test_plimit_factorizations(sort, primes): intervals = librosa.plimit_intervals( primes=primes, bins_per_octave=20, sort=sort, return_factors=False ) factors = librosa.plimit_intervals( primes=primes, bins_per_octave=20, sort=sort, return_factors=True ) assert len(intervals) == len(factors) for ival, facts in zip(intervals, factors): value = 0.0 for prime in facts: value += facts[prime] * np.log2(prime) assert np.isclose(ival, np.power(2, value)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2332273 librosa-1.0.0/tests/test_multichannel.py0000644000175100017510000010655615236640653020076 0ustar00runnerrunner#!/usr/bin/env python # CREATED:2013-03-08 15:25:18 by Brian McFee # unit tests for multi-channel functionality # # Disable cache import os try: os.environ.pop("LIBROSA_CACHE_DIR") except KeyError: pass import librosa import glob import numpy as np import scipy.stats import pytest import warnings from unittest import mock from typing import List, Union from contextlib import nullcontext as dnr @pytest.fixture(scope="module") def y_multi(): sr = 44100 duration = 5.0 y1 = librosa.chirp(fmin=100, fmax=2000, sr=sr, duration=duration) y1 += librosa.clicks(sr=sr, times=np.arange(0, duration, 0.33), length=len(y1)) y2 = librosa.chirp(fmin=50, fmax=1000, sr=sr, duration=duration) y2 += librosa.clicks(sr=sr, times=np.arange(0, duration, 0.5), length=len(y2)) y = np.vstack([y1, y2]) return y, sr @pytest.fixture(scope="module") def s_multi(y_multi): y, sr = y_multi return np.abs(librosa.stft(y)), sr @pytest.fixture(scope="module") def tfr_multi(y_multi): y, sr = y_multi return librosa.reassigned_spectrogram(y, fill_nan=True) @pytest.mark.parametrize("aggregate", [None, np.mean, np.sum]) @pytest.mark.parametrize( "ndim,axis", [ (1, 0), (1, -1), (2, 0), (2, 1), (2, -1), (3, 0), (3, 2), (3, -1), (4, 0), (4, 3), (4, -1), ], ) def test_sync_multi(aggregate, ndim: int, axis: int): data = np.ones([6] * ndim, dtype=float) # Make some slices that don't fill the entire dimension slices = [slice(1, 3), slice(3, 4)] dsync = librosa.util.sync(data, slices, aggregate=aggregate, axis=axis) # Check the axis shapes assert dsync.shape[axis] == len(slices) s_test = list(dsync.shape) del s_test[axis] s_orig = list(data.shape) del s_orig[axis] assert s_test == s_orig # The first slice will sum to 2 and have mean 1 idx: List[Union[slice, int]] = [slice(None)] * ndim idx[axis] = 0 if aggregate is np.sum: assert np.allclose(dsync[tuple(idx)], 2) else: assert np.allclose(dsync[tuple(idx)], 1) # The second slice will sum to 1 and have mean 1 idx[axis] = 1 assert np.allclose(dsync[tuple(idx)], 1) def test_stft_multi(y_multi): # Verify that a stereo STFT matches on # each channel individually y, sr = y_multi D = librosa.stft(y) D0 = librosa.stft(y[0]) D1 = librosa.stft(y[1]) # Check each channel assert np.allclose(D[0], D0) assert np.allclose(D[1], D1) # Check that they're not both the same assert not np.allclose(D0, D1) def test_onset_strength(y_multi): # Verify that a stereo spectral flux onset strength envelope matches on # each channel individually y, sr = y_multi S = librosa.stft(y) D = librosa.onset.onset_strength(S=S) D0 = librosa.onset.onset_strength(S=S[0]) D1 = librosa.onset.onset_strength(S=S[1]) # Check each channel assert np.allclose(D[0], D0) assert np.allclose(D[1], D1) # Check that they're not both the same assert not np.allclose(D0, D1) def test_tempogram(s_multi): # Verify that a stereo tempogram matches on # each channel individually S, sr = s_multi D = librosa.onset.onset_strength(S=S) t = librosa.feature.tempogram(y=None, sr=sr, onset_envelope=D, hop_length=512) D0 = librosa.onset.onset_strength(S=S[0]) D1 = librosa.onset.onset_strength(S=S[1]) t0 = librosa.feature.tempogram(y=None, sr=sr, onset_envelope=D0, hop_length=512) t1 = librosa.feature.tempogram(y=None, sr=sr, onset_envelope=D1, hop_length=512) # Check each channel assert np.allclose(t[0], t0) assert np.allclose(t[1], t1) # Check that they're not both the same assert not np.allclose(t0, t1) def test_fourier_tempogram(s_multi): # Verify that a stereo fourier tempogram matches on # each channel individually S, sr = s_multi D = librosa.onset.onset_strength(S=S) t = librosa.feature.fourier_tempogram(sr=sr, onset_envelope=D) D0 = librosa.onset.onset_strength(S=S[0]) D1 = librosa.onset.onset_strength(S=S[1]) t0 = librosa.feature.fourier_tempogram(sr=sr, onset_envelope=D0) t1 = librosa.feature.fourier_tempogram(sr=sr, onset_envelope=D1) # Check each channel assert np.allclose(t[0], t0, atol=1e-6, rtol=1e-6) assert np.allclose(t[1], t1, atol=1e-6, rtol=1e-6) # Check that they're not both the same assert not np.allclose(t0, t1, atol=1e-6, rtol=1e-6) def test_tempo_multi(y_multi): sr = 22050 tempi = [78, 128] y = np.zeros((2, 20 * sr)) delay = [librosa.time_to_samples(60 / tempo, sr=sr).item() for tempo in tempi] y[0, :: delay[0]] = 1 y[1, :: delay[1]] = 1 t = librosa.feature.tempo( y=y, sr=sr, hop_length=512, ac_size=4, aggregate=np.mean, prior=None ) t0 = librosa.feature.tempo( y=y[0], sr=sr, hop_length=512, ac_size=4, aggregate=np.mean, prior=None ) t1 = librosa.feature.tempo( y=y[1], sr=sr, hop_length=512, ac_size=4, aggregate=np.mean, prior=None ) # Check each channel assert np.allclose(t[0], t0) assert np.allclose(t[1], t1) # Check that they're not both the same assert not np.allclose(t0, t1) @pytest.mark.parametrize("hop_length", [512]) @pytest.mark.parametrize("win_length", [384]) @pytest.mark.parametrize( "tempo_min,tempo_max", [ (30, 300), (60, None), ], ) @pytest.mark.parametrize( "prior", [None, scipy.stats.lognorm(s=1, loc=np.log(120), scale=120)] ) def test_plp_multi(s_multi, hop_length, win_length, tempo_min, tempo_max, prior): S, sr = s_multi D = librosa.onset.onset_strength(S=S, sr=sr, hop_length=hop_length) D0 = librosa.onset.onset_strength(S=S[0], sr=sr, hop_length=hop_length) D1 = librosa.onset.onset_strength(S=S[1], sr=sr, hop_length=hop_length) pulse = librosa.beat.plp( sr=sr, onset_envelope=D, hop_length=hop_length, win_length=win_length, tempo_min=tempo_min, tempo_max=tempo_max, prior=prior, ) pulse0 = librosa.beat.plp( sr=sr, onset_envelope=D0, hop_length=hop_length, win_length=win_length, tempo_min=tempo_min, tempo_max=tempo_max, prior=prior, ) pulse1 = librosa.beat.plp( sr=sr, onset_envelope=D1, hop_length=hop_length, win_length=win_length, tempo_min=tempo_min, tempo_max=tempo_max, prior=prior, ) # Check each channel assert np.allclose(pulse[0], pulse0, atol=1e-6, rtol=1e-6) assert np.allclose(pulse[1], pulse1, atol=1e-6, rtol=1e-6) # Check that they're not both the same assert not np.allclose(pulse0, pulse1, atol=1e-6, rtol=1e-6) def test_istft_multi(y_multi): # Verify that a stereo ISTFT matches on each channel y, sr = y_multi # Assume the forward transform works properly in stereo D = librosa.stft(y) # Invert per channel y0m = librosa.istft(D[0]) y1m = librosa.istft(D[1]) # Invert both channels at once ys = librosa.istft(D) # Check each channel assert np.allclose(y0m, ys[0]) assert np.allclose(y1m, ys[1]) # Check that they're not both the same assert not np.allclose(ys[0], ys[1]) def test_griffinlim_multi(y_multi): y, sr = y_multi # Compute the stft D = librosa.stft(y) # Run a couple of iterations of griffin-lim yout = librosa.griffinlim(np.abs(D), n_iter=2, length=y.shape[-1]) # Check the lengths assert np.allclose(y.shape, yout.shape) @pytest.mark.parametrize("scale", [False, True]) @pytest.mark.parametrize("res_type", ["polyphase"]) def test_cqt_multi(y_multi, scale, res_type): y, sr = y_multi # Assuming single-channel CQT is well behaved C0 = librosa.cqt(y=y[0], sr=sr, scale=scale, res_type=res_type) C1 = librosa.cqt(y=y[1], sr=sr, scale=scale, res_type=res_type) Call = librosa.cqt(y=y, sr=sr, scale=scale, res_type=res_type) # Check each channel assert np.allclose(C0, Call[0]) assert np.allclose(C1, Call[1]) # Verify that they're not all the same assert not np.allclose(Call[0], Call[1]) @pytest.mark.parametrize("scale", [False, True]) @pytest.mark.parametrize("res_type", ["polyphase"]) def test_hybrid_cqt_multi(y_multi, scale, res_type): y, sr = y_multi # Assuming single-channel CQT is well behaved C0 = librosa.hybrid_cqt(y=y[0], sr=sr, scale=scale, res_type=res_type) C1 = librosa.hybrid_cqt(y=y[1], sr=sr, scale=scale, res_type=res_type) Call = librosa.hybrid_cqt(y=y, sr=sr, scale=scale, res_type=res_type) # Check each channel assert np.allclose(C0, Call[0]) assert np.allclose(C1, Call[1]) # Verify that they're not all the same assert not np.allclose(Call[0], Call[1]) @pytest.mark.parametrize("scale", [False, True]) @pytest.mark.parametrize("length", [None, 22050]) def test_icqt_multi(y_multi, scale, length): y, sr = y_multi # Assuming the forward transform is well-behaved C = librosa.cqt(y=y, sr=sr, scale=scale) yboth = librosa.icqt(C, sr=sr, scale=scale, length=length) y0 = librosa.icqt(C[0], sr=sr, scale=scale, length=length) y1 = librosa.icqt(C[1], sr=sr, scale=scale, length=length) if length is not None: assert yboth.shape[-1] == length # Check each channel - slightly relaxed tolerance here assert np.allclose(yboth[0], y0, atol=1e-6), np.max(np.abs(yboth[0] - y0)) assert np.allclose(yboth[1], y1, atol=1e-6), np.max(np.abs(yboth[1] - y1)) # Check that they're not the same assert not np.allclose(yboth[0], yboth[1]) def test_griffinlim_cqt_multi(y_multi): y, sr = y_multi # Compute the stft C = librosa.cqt(y, sr=sr) # Run a couple of iterations of griffin-lim yout = librosa.griffinlim_cqt(np.abs(C), n_iter=2, length=y.shape[-1]) # Check the lengths assert np.allclose(y.shape, yout.shape) def test_spectral_centroid_multi(s_multi): S, sr = s_multi freq = None # Assuming single-channel CQT is well behaved C0 = librosa.feature.spectral_centroid(sr=sr, freq=freq, S=S[0]) C1 = librosa.feature.spectral_centroid(sr=sr, freq=freq, S=S[1]) Call = librosa.feature.spectral_centroid(sr=sr, freq=freq, S=S) # Check each channel assert np.allclose(C0, Call[0]) assert np.allclose(C1, Call[1]) # Verify that they're not all the same assert not np.allclose(Call[0], Call[1]) def test_spectral_centroid_multi_variable(s_multi, rng): S, sr = s_multi freq = np.asarray(rng.standard_normal(size=S.shape)) # compare each channel C0 = librosa.feature.spectral_centroid(sr=sr, freq=freq[0], S=S[0]) C1 = librosa.feature.spectral_centroid(sr=sr, freq=freq[1], S=S[1]) Call = librosa.feature.spectral_centroid(sr=sr, freq=freq, S=S) # Check each channel assert np.allclose(C0, Call[0]) assert np.allclose(C1, Call[1]) # Verify that they're not all the same assert not np.allclose(Call[0], Call[1]) def test_spectral_bandwidth_multi(s_multi): S, sr = s_multi freq = None # compare each channel C0 = librosa.feature.spectral_bandwidth(sr=sr, freq=freq, S=S[0]) C1 = librosa.feature.spectral_bandwidth(sr=sr, freq=freq, S=S[1]) Call = librosa.feature.spectral_bandwidth(sr=sr, freq=freq, S=S) # Check each channel assert np.allclose(C0, Call[0]) assert np.allclose(C1, Call[1]) # Verify that they're not all the same assert not np.allclose(Call[0], Call[1]) def test_spectral_bandwidth_multi_variable(s_multi, rng): S, sr = s_multi freq = np.asarray(rng.standard_normal(size=S.shape)) # compare each channel C0 = librosa.feature.spectral_bandwidth(sr=sr, freq=freq[0], S=S[0]) C1 = librosa.feature.spectral_bandwidth(sr=sr, freq=freq[1], S=S[1]) Call = librosa.feature.spectral_bandwidth(sr=sr, freq=freq, S=S) # Check each channel assert np.allclose(C0, Call[0]) assert np.allclose(C1, Call[1]) # Verify that they're not all the same assert not np.allclose(Call[0], Call[1]) def test_spectral_contrast_multi(s_multi): S, sr = s_multi freq = None # compare each channel # FIXME: using linear mode because dB scaling has cross-channel sensitivities C0 = librosa.feature.spectral_contrast(sr=sr, freq=freq, S=S[0], linear=True) C1 = librosa.feature.spectral_contrast(sr=sr, freq=freq, S=S[1], linear=True) Call = librosa.feature.spectral_contrast(sr=sr, freq=freq, S=S, linear=True) # Check each channel assert np.allclose(C0, Call[0]) assert np.allclose(C1, Call[1]) # Verify that they're not all the same assert not np.allclose(Call[0], Call[1]) def test_spectral_rolloff_multi(s_multi): S, sr = s_multi freq = None # compare each channel C0 = librosa.feature.spectral_rolloff(sr=sr, freq=freq, S=S[0]) C1 = librosa.feature.spectral_rolloff(sr=sr, freq=freq, S=S[1]) Call = librosa.feature.spectral_rolloff(sr=sr, freq=freq, S=S) # Check each channel assert np.allclose(C0, Call[0]) assert np.allclose(C1, Call[1]) # Verify that they're not all the same assert not np.allclose(Call[0], Call[1]) def test_spectral_rolloff_multi_variable(s_multi, rng): S, sr = s_multi freq = np.asarray(rng.standard_normal(size=S.shape)) # compare each channel C0 = librosa.feature.spectral_rolloff(sr=sr, freq=freq[0], S=S[0]) C1 = librosa.feature.spectral_rolloff(sr=sr, freq=freq[1], S=S[1]) Call = librosa.feature.spectral_rolloff(sr=sr, freq=freq, S=S) # Check each channel assert np.allclose(C0, Call[0]) assert np.allclose(C1, Call[1]) # Verify that they're not all the same assert not np.allclose(Call[0], Call[1]) def test_spectral_flatness_multi(s_multi): S, sr = s_multi # compare each channel C0 = librosa.feature.spectral_flatness(S=S[0]) C1 = librosa.feature.spectral_flatness(S=S[1]) Call = librosa.feature.spectral_flatness(S=S) # Check each channel assert np.allclose(C0, Call[0], atol=1e-5) assert np.allclose(C1, Call[1]) # Verify that they're not all the same assert not np.allclose(Call[0], Call[1]) def test_poly_multi_static(s_multi): mags, sr = s_multi Pall = librosa.feature.poly_features(S=mags, order=5) # Compute per channel P0 = librosa.feature.poly_features(S=mags[0], order=5) P1 = librosa.feature.poly_features(S=mags[1], order=5) # Check results assert np.allclose(Pall[0], P0) assert np.allclose(Pall[1], P1) assert not np.allclose(P0, P1) # Not worried about polyfit conditioning for this test @pytest.mark.filterwarnings("ignore:Polyfit may be poorly conditioned") def test_poly_multi_varying(tfr_multi): # Get some time-varying frequencies times, freqs, mags = tfr_multi Pall = librosa.feature.poly_features(S=mags, freq=freqs, order=5) # Compute per channel P0 = librosa.feature.poly_features(S=mags[0], freq=freqs[0], order=5) P1 = librosa.feature.poly_features(S=mags[1], freq=freqs[1], order=5) # Check results assert np.allclose(Pall[0], P0) assert np.allclose(Pall[1], P1) assert not np.allclose(P0, P1) def test_rms_multi(s_multi): S, sr = s_multi # compare each channel C0 = librosa.feature.rms(S=S[0]) C1 = librosa.feature.rms(S=S[1]) Call = librosa.feature.rms(S=S) assert Call.ndim == 3 # Check each channel assert np.allclose(C0, Call[0]) assert np.allclose(C1, Call[1]) # Verify that they're not all the same assert not np.allclose(Call[0], Call[1]) def test_zcr_multi(y_multi): y, sr = y_multi # compare each channel C0 = librosa.feature.zero_crossing_rate(y=y[0]) C1 = librosa.feature.zero_crossing_rate(y=y[1]) Call = librosa.feature.zero_crossing_rate(y=y) # Check each channel assert np.allclose(C0, Call[0]) assert np.allclose(C1, Call[1]) # Verify that they're not all the same assert not np.allclose(Call[0], Call[1]) def test_chroma_stft_multi(s_multi): S, sr = s_multi # compare each channel C0 = librosa.feature.chroma_stft(S=S[0], tuning=0) C1 = librosa.feature.chroma_stft(S=S[1], tuning=0) Call = librosa.feature.chroma_stft(S=S, tuning=0) # Check each channel assert np.allclose(C0, Call[0]) assert np.allclose(C1, Call[1]) # Verify that they're not all the same assert not np.allclose(Call[0], Call[1]) def test_chroma_cqt_multi(y_multi): y, sr = y_multi # compare each channel C0 = librosa.feature.chroma_cqt(y=y[0], tuning=0) C1 = librosa.feature.chroma_cqt(y=y[1], tuning=0) Call = librosa.feature.chroma_cqt(y=y, tuning=0) # Check each channel assert np.allclose(C0, Call[0]) assert np.allclose(C1, Call[1]) # Verify that they're not all the same assert not np.allclose(Call[0], Call[1]) def test_chroma_cens_multi(y_multi): y, sr = y_multi # compare each channel C0 = librosa.feature.chroma_cens(y=y[0], tuning=0) C1 = librosa.feature.chroma_cens(y=y[1], tuning=0) Call = librosa.feature.chroma_cens(y=y, tuning=0) # Check each channel assert np.allclose(C0, Call[0]) assert np.allclose(C1, Call[1]) # Verify that they're not all the same assert not np.allclose(Call[0], Call[1]) def test_tonnetz_multi(y_multi): y, sr = y_multi # compare each channel C0 = librosa.feature.tonnetz(y=y[0], tuning=0) C1 = librosa.feature.tonnetz(y=y[1], tuning=0) Call = librosa.feature.tonnetz(y=y, tuning=0) # Check each channel assert np.allclose(C0, Call[0], atol=1e-7) assert np.allclose(C1, Call[1], atol=1e-7) # Verify that they're not all the same assert not np.allclose(Call[0], Call[1]) def test_mfcc_multi(s_multi): S, sr = s_multi # compare each channel C0 = librosa.feature.mfcc(S=librosa.core.amplitude_to_db(S=S[0], top_db=None)) C1 = librosa.feature.mfcc(S=librosa.core.amplitude_to_db(S=S[1], top_db=None)) Call = librosa.feature.mfcc(S=librosa.core.amplitude_to_db(S=S, top_db=None)) # Check each channel assert np.allclose(C0, Call[0]) assert np.allclose(C1, Call[1]) # Verify that they're not all the same assert not np.allclose(Call[0], Call[1]) def test_mfcc_multi_time(y_multi): y, sr = y_multi # compare each channel C0 = librosa.feature.mfcc(y=y[0], sr=sr) C1 = librosa.feature.mfcc(y=y[1], sr=sr) Call = librosa.feature.mfcc(y=y, sr=sr) # Verify that they're not all the same assert not np.allclose(Call[0], Call[1]) # Check each channel assert np.allclose(C0, Call[0]), np.max(np.abs(C0 - Call[0])) assert np.allclose(C1, Call[1]), np.max(np.abs(C1 - Call[1])) def test_melspectrogram_multi(s_multi): S, sr = s_multi # compare each channel C0 = librosa.feature.melspectrogram(S=S[0]) C1 = librosa.feature.melspectrogram(S=S[1]) Call = librosa.feature.melspectrogram(S=S) # Check each channel assert np.allclose(C0, Call[0]) assert np.allclose(C1, Call[1]) # Verify that they're not all the same assert not np.allclose(Call[0], Call[1]) def test_melspectrogram_multi_time(y_multi): y, sr = y_multi # compare each channel C0 = librosa.feature.melspectrogram(y=y[0]) C1 = librosa.feature.melspectrogram(y=y[1]) Call = librosa.feature.melspectrogram(y=y) # Check each channel assert np.allclose(C0, Call[0]) assert np.allclose(C1, Call[1]) # Verify that they're not all the same assert not np.allclose(Call[0], Call[1]) @pytest.mark.parametrize("rate", [0.5, 2]) def test_phase_vocoder(y_multi, rate): y, sr = y_multi D = librosa.stft(y) D0 = librosa.phase_vocoder(D[0], rate=rate) D1 = librosa.phase_vocoder(D[1], rate=rate) D2 = librosa.phase_vocoder(D, rate=rate) assert np.allclose(D2[0], D0) assert np.allclose(D2[1], D1) assert not np.allclose(D2[0], D2[1]) @pytest.mark.parametrize("delay", [1, -1]) def test_stack_memory_multi(delay, rng): data = rng.standard_normal(size=(2, 5, 200)) # compare each channel C0 = librosa.feature.stack_memory(data[0], delay=delay) C1 = librosa.feature.stack_memory(data[1], delay=delay) Call = librosa.feature.stack_memory(data, delay=delay) # Check each channel assert np.allclose(C0, Call[0]) assert np.allclose(C1, Call[1]) # Verify that they're not all the same assert not np.allclose(Call[0], Call[1]) def test_interp_harmonics_multi_static(s_multi): S, sr = s_multi freqs = librosa.fft_frequencies(sr=sr) Hall = librosa.interp_harmonics(S, freqs=freqs, harmonics=[0.5, 1, 2]) H0 = librosa.interp_harmonics(S[0], freqs=freqs, harmonics=[0.5, 1, 2]) H1 = librosa.interp_harmonics(S[1], freqs=freqs, harmonics=[0.5, 1, 2]) assert np.allclose(Hall[0], H0) assert np.allclose(Hall[1], H1) assert not np.allclose(H0, H1) # Not worried about this warning here @pytest.mark.filterwarnings("ignore:Frequencies are not unique") def test_interp_harmonics_multi_vary(tfr_multi): times, freqs, mags = tfr_multi Hall = librosa.interp_harmonics( mags, freqs=freqs, harmonics=[0.5, 1, 2], kind="nearest" ) H0 = librosa.interp_harmonics( mags[0], freqs=freqs[0], harmonics=[0.5, 1, 2], kind="nearest" ) H1 = librosa.interp_harmonics( mags[1], freqs=freqs[1], harmonics=[0.5, 1, 2], kind="nearest" ) assert np.allclose(Hall[0], H0) assert np.allclose(Hall[1], H1) assert not np.allclose(H0, H1) @pytest.mark.parametrize("filter_peaks", [False, True]) def test_salience_multi_static(s_multi, filter_peaks): S, sr = s_multi freqs = librosa.fft_frequencies(sr=sr) sal_all = librosa.salience( S, freqs=freqs, harmonics=[0.5, 1, 2, 3], kind="slinear", filter_peaks=filter_peaks, fill_value=0, ) sal_0 = librosa.salience( S[0], freqs=freqs, harmonics=[0.5, 1, 2, 3], kind="slinear", filter_peaks=filter_peaks, fill_value=0, ) sal_1 = librosa.salience( S[1], freqs=freqs, harmonics=[0.5, 1, 2, 3], kind="slinear", filter_peaks=filter_peaks, fill_value=0, ) assert np.allclose(sal_all[0], sal_0) assert np.allclose(sal_all[1], sal_1) assert not np.allclose(sal_0, sal_1) @pytest.mark.parametrize("filter_peaks", [False, True]) # Not worried about this warning here @pytest.mark.filterwarnings("ignore:Frequencies are not unique") def test_salience_multi_dynamic(tfr_multi, filter_peaks): times, freqs, S = tfr_multi sal_all = librosa.salience( S, freqs=freqs, harmonics=[0.5, 1, 2, 3], kind="nearest", filter_peaks=filter_peaks, fill_value=0, ) sal_0 = librosa.salience( S[0], freqs=freqs[0], harmonics=[0.5, 1, 2, 3], kind="nearest", filter_peaks=filter_peaks, fill_value=0, ) sal_1 = librosa.salience( S[1], freqs=freqs[1], harmonics=[0.5, 1, 2, 3], kind="nearest", filter_peaks=filter_peaks, fill_value=0, ) assert np.allclose(sal_all[0], sal_0) assert np.allclose(sal_all[1], sal_1) assert not np.allclose(sal_0, sal_1) @pytest.mark.parametrize("center", [False, True]) def test_iirt_multi(y_multi, center): y, sr = y_multi Call = librosa.iirt(y=y, sr=sr, center=center) C0 = librosa.iirt(y=y[0], sr=sr, center=center) C1 = librosa.iirt(y=y[1], sr=sr, center=center) assert np.allclose(Call[0], C0) assert np.allclose(Call[1], C1) assert not np.allclose(C0, C1) def test_lpc_multi(y_multi): y, sr = y_multi Lall = librosa.lpc(y, order=6) L0 = librosa.lpc(y[0], order=6) L1 = librosa.lpc(y[1], order=6) assert np.allclose(Lall[0], L0) assert np.allclose(Lall[1], L1) assert not np.allclose(L0, L1) def test_yin_multi(y_multi): y, sr = y_multi Pall = librosa.yin(y, fmin=30, fmax=300) P0 = librosa.yin(y[0], fmin=30, fmax=300) P1 = librosa.yin(y[1], fmin=30, fmax=300) assert np.allclose(Pall[0], P0) assert np.allclose(Pall[1], P1) assert not np.allclose(P0, P1) @pytest.mark.parametrize("ref", [None, 1.0]) def test_piptrack_multi(s_multi, ref): S, sr = s_multi pall, mall = librosa.piptrack(S=S, sr=sr, ref=ref) p0, m0 = librosa.piptrack(S=S[0], sr=sr, ref=ref) p1, m1 = librosa.piptrack(S=S[1], sr=sr, ref=ref) assert np.allclose(pall[0], p0) assert np.allclose(pall[1], p1) assert np.allclose(mall[0], m0) assert np.allclose(mall[1], m1) assert not np.allclose(p0, p1) assert not np.allclose(m0, m1) def test_click_multi(): click = np.ones((3, 100)) yout = librosa.clicks(times=[0, 1, 2], sr=1000, click=click) assert yout.shape[0] == click.shape[0] assert np.allclose(yout[..., :100], click) assert np.allclose(yout[..., 1000:1100], click) assert np.allclose(yout[..., 2000:2100], click) def test_nnls_multi(s_multi): # Verify that a stereo melspectrogram can be reconstructed # for each channel individually S, sr = s_multi S = S[..., : int(S.shape[-1] / 2)] # multichannel mel_basis = librosa.filters.mel(sr=sr, n_fft=2 * S.shape[-2] - 1, n_mels=256) M = np.einsum("...ft,mf->...mt", S, mel_basis) S_recover = librosa.util.nnls(mel_basis, M) # channel 0 M0 = np.einsum("...ft,mf->...mt", S[0], mel_basis) S0_recover = librosa.util.nnls(mel_basis, M0) # channel 1 M1 = np.einsum("...ft,mf->...mt", S[1], mel_basis) S1_recover = librosa.util.nnls(mel_basis, M1) # Check each channel assert np.allclose(S_recover[0], S0_recover, atol=1e-5, rtol=1e-5), np.max( np.abs(S_recover[0] - S0_recover) ) assert np.allclose(S_recover[1], S1_recover, atol=1e-5, rtol=1e-5), np.max( np.abs(S_recover[1] - S1_recover) ) # Check that they're not both the same assert not np.allclose(S0_recover, S1_recover) # -- feature inversion tests @pytest.mark.parametrize("power", [1, 2]) @pytest.mark.parametrize("n_fft", [1024, 2048]) def test_mel_to_stft_multi(power, n_fft, rng): # Make a random mel spectrum, 4 frames mel_basis = librosa.filters.mel(sr=22050, n_fft=n_fft, n_mels=128) stft_orig = rng.standard_normal(size=(2, n_fft // 2 + 1, 4)) ** power mels = np.einsum("...ft,mf->...mt", stft_orig, mel_basis) stft = librosa.feature.inverse.mel_to_stft(mels, power=power, n_fft=n_fft) mels0 = np.einsum("...ft,mf->...mt", stft_orig[0], mel_basis) stft0 = librosa.feature.inverse.mel_to_stft(mels0, power=power, n_fft=n_fft) mels1 = np.einsum("...ft,mf->...mt", stft_orig[1], mel_basis) stft1 = librosa.feature.inverse.mel_to_stft(mels1, power=power, n_fft=n_fft) # Check each channel assert np.allclose(stft[0], stft0) assert np.allclose(stft[1], stft1) # Check that they're not both the same assert not np.allclose(stft0, stft1) @pytest.mark.parametrize("n_mfcc", [13, 20]) @pytest.mark.parametrize("n_mels", [64, 128]) @pytest.mark.parametrize("dct_type", [2, 3]) def test_mfcc_to_mel_multi(s_multi, n_mfcc, n_mels, dct_type): S, sr = s_multi # compare each channel mfcc0 = librosa.feature.mfcc(S=librosa.core.amplitude_to_db(S=S[0], top_db=None)) mfcc1 = librosa.feature.mfcc(S=librosa.core.amplitude_to_db(S=S[1], top_db=None)) mfcc = librosa.feature.mfcc(S=librosa.core.amplitude_to_db(S=S, top_db=None)) mel_recover = librosa.feature.inverse.mfcc_to_mel( mfcc, n_mels=n_mels, dct_type=dct_type ) mel_recover0 = librosa.feature.inverse.mfcc_to_mel( mfcc0, n_mels=n_mels, dct_type=dct_type ) mel_recover1 = librosa.feature.inverse.mfcc_to_mel( mfcc1, n_mels=n_mels, dct_type=dct_type ) # Check each channel assert np.allclose(mel_recover[0], mel_recover0) assert np.allclose(mel_recover[1], mel_recover1) # Check that they're not both the same assert not np.allclose(mel_recover0, mel_recover1) def test_trim_multichannel(y_multi): y, sr = y_multi # Make one channel much quieter than the other y = y * np.array([[1e-6, 1e6]]).T yt, ival = librosa.effects.trim(y) yt0, ival0 = librosa.effects.trim(y[0]) yt1, ival1 = librosa.effects.trim(y[1]) # Trim uses max aggregation across channels by default # So the multichannel trimming window will be the # intersection of the individual intervals assert ival[0] == max(ival0[0], ival1[0]) assert ival[1] == min(ival0[1], ival1[1]) @pytest.mark.parametrize( "res_type", ("scipy", "polyphase", "sinc_fastest", "kaiser_fast", "soxr_qq") ) def test_resample_multichannel(y_multi, res_type): # Test multi-channel resampling with all backends: scipy, samplerate, resampy, soxr y, sr = y_multi y_res = librosa.resample(y=y, orig_sr=sr, target_sr=sr // 2, res_type=res_type) y0_res = librosa.resample(y=y[0], orig_sr=sr, target_sr=sr // 2, res_type=res_type) y1_res = librosa.resample(y=y[1], orig_sr=sr, target_sr=sr // 2, res_type=res_type) assert np.allclose(y_res[0], y0_res) assert np.allclose(y_res[1], y1_res) assert y_res[0].shape == y0_res.shape @pytest.mark.parametrize( "res_type", ("scipy", "polyphase", "sinc_fastest", "kaiser_fast", "soxr_qq") ) @pytest.mark.parametrize("x", [np.zeros((2, 2, 2, 22050))]) def test_resample_highdim(x, res_type): # Just run these to verify that it doesn't crash with ndim>2 y = librosa.resample(x, orig_sr=22050, target_sr=11025, res_type=res_type) @pytest.mark.parametrize( "res_type", ("scipy", "polyphase", "sinc_fastest", "kaiser_fast", "soxr_qq") ) @pytest.mark.parametrize( "x, axis", [(np.zeros((2, 2, 2, 22050)), -1), (np.zeros((22050, 2, 3)), 0)] ) def test_resample_highdim_axis(x, axis, res_type): # Resample along the target axis y = librosa.resample( x, orig_sr=22050, target_sr=11025, axis=axis, res_type=res_type ) # Verify that the target axis is the correct shape assert y.shape[axis] == 11025 assert y.ndim == x.ndim @pytest.mark.parametrize("dynamic", [False, True]) # Not worried about this warning here @pytest.mark.filterwarnings("ignore:Frequencies are not unique") def test_f0_harmonics(y_multi, dynamic, rng): y, sr = y_multi Df, _, S = librosa.reassigned_spectrogram(y, sr=sr, fill_nan=True) freqs = librosa.fft_frequencies(sr=sr) harmonics = np.array([1, 2, 3]) f0 = 100 + 30 * rng.random(size=(S.shape[0], S.shape[-1])) if dynamic: out = librosa.f0_harmonics(S, freqs=Df, f0=f0, harmonics=harmonics) out0 = librosa.f0_harmonics(S[0], freqs=Df[0], f0=f0[0], harmonics=harmonics) out1 = librosa.f0_harmonics(S[1], freqs=Df[1], f0=f0[1], harmonics=harmonics) else: out = librosa.f0_harmonics(S, freqs=freqs, f0=f0, harmonics=harmonics) out0 = librosa.f0_harmonics(S[0], freqs=freqs, f0=f0[0], harmonics=harmonics) out1 = librosa.f0_harmonics(S[1], freqs=freqs, f0=f0[1], harmonics=harmonics) assert np.allclose(out[0], out0) assert np.allclose(out[1], out1) @pytest.mark.parametrize("method", ["greedy", "dp_count", "dp_value"]) def test_peak_pick_multi(method, rng): x = rng.standard_normal(size=(3, 1000)) ** 2 pre_max = 5 post_max = 5 pre_avg = 10 post_avg = 10 wait = 10 delta = 0.5 pm = librosa.util.peak_pick( x, pre_max=pre_max, post_max=post_max, pre_avg=pre_avg, post_avg=post_avg, wait=wait, delta=delta, sparse=False, axis=-1, method=method, ) for i in range(x.shape[0]): pmi = librosa.util.peak_pick( x[i], pre_max=pre_max, post_max=post_max, pre_avg=pre_avg, post_avg=post_avg, wait=wait, delta=delta, sparse=False, axis=-1, method=method, ) assert np.allclose(pmi, pm[i]) def test_peak_pick_axis(rng): x = rng.standard_normal(size=(100, 500)) ** 2 pre_max = 5 post_max = 5 pre_avg = 10 post_avg = 10 wait = 10 delta = 0.5 peaks = librosa.util.peak_pick( x, pre_max=pre_max, post_max=post_max, pre_avg=pre_avg, post_avg=post_avg, wait=wait, delta=delta, sparse=False, axis=-1, ) peaks_t = librosa.util.peak_pick( x.T, pre_max=pre_max, post_max=post_max, pre_avg=pre_avg, post_avg=post_avg, wait=wait, delta=delta, sparse=False, axis=0, ) assert np.allclose(peaks_t.T, peaks) @pytest.mark.xfail(raises=librosa.ParameterError) def test_peak_pick_multi_fail(rng): x = rng.standard_normal(size=(3, 1000)) ** 2 pre_max = 5 post_max = 5 pre_avg = 10 post_avg = 10 wait = 10 delta = 0.5 pm = librosa.util.peak_pick( x, pre_max=pre_max, post_max=post_max, pre_avg=pre_avg, post_avg=post_avg, wait=wait, delta=delta, sparse=True, axis=-1, ) def test_onset_detect(y_multi): # Verify that a stereo onset detection matches each channel individually y, sr = y_multi # Pre-compute the onset strength here using both channels to avoid any # channel normalization issues if we were to process fully independently oenv = librosa.onset.onset_strength(y=y, sr=sr) D = librosa.onset.onset_detect(onset_envelope=oenv, sr=sr, sparse=False) D0 = librosa.onset.onset_detect(onset_envelope=oenv[0], sr=sr, sparse=False) D1 = librosa.onset.onset_detect(onset_envelope=oenv[1], sr=sr, sparse=False) # Check each channel assert np.allclose(D[0], D0) assert np.allclose(D[1], D1) # Check that they're not both the same assert not np.allclose(D0, D1) @pytest.mark.parametrize("trim", [False, True]) def test_beat_track_multi(y_multi, trim): y, sr = y_multi tempo, beats = librosa.beat.beat_track(y=y, sr=sr, trim=trim, sparse=False) tempo0, beats0 = librosa.beat.beat_track(y=y[0], sr=sr, trim=trim, sparse=False) tempo1, beats1 = librosa.beat.beat_track(y=y[1], sr=sr, trim=trim, sparse=False) assert isinstance(tempo, np.ndarray) assert np.allclose(tempo[0], tempo0) assert np.allclose(tempo[1], tempo1) assert np.allclose(beats[0], beats0) assert np.allclose(beats[1], beats1) def test_beat_track_multi_bpm_scalar(y_multi): y, sr = y_multi tempo, beats = librosa.beat.beat_track(y=y, sr=sr, sparse=False, bpm=100) tempo0, beats0 = librosa.beat.beat_track(y=y[0], sr=sr, sparse=False, bpm=100) tempo1, beats1 = librosa.beat.beat_track(y=y[1], sr=sr, sparse=False, bpm=100) assert np.isscalar(tempo) assert np.allclose(tempo, tempo0) assert np.allclose(tempo, tempo1) assert np.allclose(beats[0], beats0) assert np.allclose(beats[1], beats1) def test_beat_track_multi_bpm_vector(y_multi): y, sr = y_multi bpm = np.array([[150], [75]]) assert isinstance(bpm, np.ndarray) tempo, beats = librosa.beat.beat_track(y=y, sr=sr, sparse=False, bpm=bpm) tempo0, beats0 = librosa.beat.beat_track(y=y[0], sr=sr, sparse=False, bpm=bpm[0]) tempo1, beats1 = librosa.beat.beat_track(y=y[1], sr=sr, sparse=False, bpm=bpm[1]) assert isinstance(tempo, np.ndarray) assert np.allclose(tempo, bpm) assert np.allclose(tempo[0], tempo0) assert np.allclose(tempo[1], tempo1) assert np.allclose(beats[0], beats0) assert np.allclose(beats[1], beats1) @pytest.mark.xfail(raises=librosa.ParameterError) def test_beat_track_multi_sparse(y_multi): y, sr = y_multi librosa.beat.beat_track(y=y, sr=sr, sparse=True) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2332273 librosa-1.0.0/tests/test_notation.py0000644000175100017510000003567315236640653017247 0ustar00runnerrunner#!/usr/bin/env python # -*- encoding: utf-8 -*- # CREATED:2020-07-16 16:49:03 by Brian McFee """Unit tests for music notations""" import os import sys try: os.environ.pop("LIBROSA_CACHE_DIR") except KeyError: pass import warnings import numpy as np import pytest import librosa @pytest.mark.xfail(raises=librosa.ParameterError) def test_key_to_notes_badkey(): librosa.key_to_notes("not a key") @pytest.mark.xfail(raises=librosa.ParameterError) def test_simplify_note_badnote(): librosa.core.notation.__simplify_note("not a note") @pytest.mark.xfail(raises=librosa.ParameterError) def test_note_to_degree_badnote(): librosa.core.notation.__note_to_degree("not a note") @pytest.mark.parametrize( "key,ref_notes", [ # Test for implicit accidentals, ties ("C:maj", ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]), ("A:min", ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]), # Test for implicit accidentals, unambiguous ("D:maj", ["Cn", "C#", "D", "D#", "E", "Fn", "F#", "G", "G#", "A", "A#", "B"]), ( "F:min", ["C", "Db", "Dn", "Eb", "En", "F", "Gb", "G", "Ab", "An", "Bb", "Bn"], ), # Test for proper enharmonics with ties ( "Eb:min", ["Cn", "Db", "Dn", "Eb", "En", "F", "Gb", "Gn", "Ab", "An", "Bb", "Cb"], ), ( "D#:min", ["Cn", "C#", "Dn", "D#", "En", "E#", "F#", "Gn", "G#", "An", "A#", "B"], ), ( "Gb:maj", ["Cn", "Db", "Dn", "Eb", "En", "F", "Gb", "Gn", "Ab", "An", "Bb", "Cb"], ), ( "F#:maj", ["Cn", "C#", "Dn", "D#", "En", "E#", "F#", "Gn", "G#", "An", "A#", "B"], ), # Test for theoretical keys ( "G#:maj", ["B#", "C#", "Dn", "D#", "En", "E#", "F#", "F##", "G#", "An", "A#", "Bn"], ), ( "Cb:min", ["Cn", "Db", "Ebb", "Eb", "Fb", "Fn", "Gb", "Abb", "Ab", "Bbb", "Bb", "Cb"], ), # Test the edge case of theoretical sharps ( "B#:maj", [ "B#", "C#", "C##", "D#", "D##", "E#", "F#", "F##", "G#", "G##", "A#", "A##", ], ), # Test for multiple accidentals in tonic name. ( "F##:maj", [ "B#", "C#", "C##", "D#", "D##", "E#", "E##", "F##", "G#", "G##", "A#", "A##", ], ), ( "Fbb:maj", [ "Dbb", "Db", "Ebb", "Fbb", "Fb", "Gbb", "Gb", "Abb", "Bbbb", "Bbb", "Cbb", "Cb", ], ), ( "A###:min", [ "A###", "B##", "B###", "C###", "D##", "D###", "E##", "E###", "F###", "G##", "G###", "A##", ], ), # Testing that the modes work. These were copied from the output generated in the discussion at https://github.com/librosa/librosa/pull/1739#issuecomment-1711949365. ( "E:ion", ["Cn", "C#", "Dn", "D#", "E", "Fn", "F#", "Gn", "G#", "A", "A#", "B"], ), ( "E#:mix", ["B#", "C#", "C##", "D#", "En", "E#", "F#", "F##", "G#", "G##", "A#", "Bn"], ), ( "E#:lyd", [ "B#", "C#", "C##", "D#", "D##", "E#", "F#", "F##", "G#", "G##", "A#", "A##", ], ), ( "Gb:dor", ["Cn", "Db", "Dn", "Eb", "Fb", "Fn", "Gb", "Gn", "Ab", "Bbb", "Bb", "Cb"], ), ( "Gb:phr", [ "Dbb", "Db", "Ebb", "Eb", "Fb", "Gbb", "Gb", "Abb", "Ab", "Bbb", "Bb", "Cb", ], ), ( "B#:aeol", ["B#", "C#", "C##", "D#", "En", "E#", "F#", "F##", "G#", "An", "A#", "Bn"], ), ( "B#:loc", ["B#", "C#", "Dn", "D#", "En", "E#", "F#", "Gn", "G#", "An", "A#", "Bn"], ), ], ) def test_key_to_notes(key, ref_notes): notes = librosa.key_to_notes(key, unicode=False, natural=True) assert len(notes) == len(ref_notes) for n, rn in zip(notes, ref_notes): assert n == rn @pytest.mark.parametrize( "key,ref_notes,natural", [ ( "G#:maj", ["B♯", "C♯", "D", "D♯", "E", "E♯", "F♯", "F�", "G♯", "A", "A♯", "B"], False, ), ( "Cb:min", ["C", "D♭", "E�", "E♭", "F♭", "F", "G♭", "A�", "A♭", "B�", "B♭", "C♭"], False, ), ( "G#:maj", ["B♯", "C♯", "D♮", "D♯", "E♮", "E♯", "F♯", "F�", "G♯", "A♮", "A♯", "B♮"], True, ), ( "G#:ion", ["B♯", "C♯", "D♮", "D♯", "E♮", "E♯", "F♯", "F�", "G♯", "A♮", "A♯", "B♮"], True, ), ], ) def test_key_to_notes_unicode(key, ref_notes, natural): notes = librosa.key_to_notes(key, unicode=True, natural=natural) assert len(notes) == len(ref_notes) for n, rn in zip(notes, ref_notes): assert n == rn @pytest.mark.parametrize( "key,ref_notes", [ ( "G#:maj", ["B♯", "C♯", "D", "D♯", "E", "E♯", "F♯", "F�", "G♯", "A", "A♯", "B"], ), ( "Cb:min", ["C", "D♭", "E�", "E♭", "F♭", "F", "G♭", "A�", "A♭", "B�", "B♭", "C♭"], ), ], ) def test_key_to_notes_no_natural(key, ref_notes): notes = librosa.key_to_notes(key, unicode=True, natural=False) assert len(notes) == len(ref_notes) for n, rn in zip(notes, ref_notes): assert n == rn @pytest.mark.parametrize( "note, ref_simplified_ascii", [ ("G####bb", "G##"), ("F#n", "F#"), ], ) def test_simplify_note_ascii(note, ref_simplified_ascii): simplified_note = librosa.core.notation.__simplify_note(note, unicode=False) for n, rn in zip(simplified_note, ref_simplified_ascii): assert n == rn @pytest.mark.parametrize( "notes, ref_simplified_array", [(["C♭♯", "C♭♭♭"], ["C", "C♭�"])], ) def test_simplify_note_array(notes, ref_simplified_array): simplified_note = librosa.core.notation.__simplify_note(notes) for n, rn in zip(simplified_note, ref_simplified_array): assert n == rn @pytest.mark.xfail(raises=librosa.ParameterError) def test_key_to_degrees_badkey(): librosa.key_to_degrees("not a key") @pytest.mark.parametrize( "key,ref_degrees", [ ("C:maj", [0, 2, 4, 5, 7, 9, 11]), ("C:min", [0, 2, 3, 5, 7, 8, 10]), ("A:min", [9, 11, 0, 2, 4, 5, 7]), ("Gb:maj", [6, 8, 10, 11, 1, 3, 5]), ("A###:maj", [0, 2, 4, 5, 7, 9, 11]), ("C:ion", [0, 2, 4, 5, 7, 9, 11]), ("C:dor", [0, 2, 3, 5, 7, 9, 10]), ("C:phr", [0, 1, 3, 5, 7, 8, 10]), ("D#:lyd", [3, 5, 7, 9, 10, 0, 2]), ("D#:mix", [3, 5, 7, 8, 10, 0, 1]), ("Ebb:aeol", [2, 4, 5, 7, 9, 10, 0]), ("Ebb:loc", [2, 3, 5, 7, 8, 10, 0]), ], ) def test_key_to_degrees(key, ref_degrees): degrees = librosa.key_to_degrees(key) assert len(degrees) == len(ref_degrees) for d, rd in zip(degrees, ref_degrees): assert d == rd @pytest.mark.xfail(raises=librosa.ParameterError) def test_mode_to_key_badkey(): librosa.core.notation.__mode_to_key("not a key") @pytest.mark.parametrize( "mode, ref_mode", [("C:maj", "C:maj")], ) def test_mode_to_key_no_change(mode, ref_mode): simplified_mode = librosa.core.notation.__mode_to_key(mode) for n, rn in zip(mode, ref_mode): assert n == rn def test_list_thaat(): thaat = librosa.list_thaat() assert len(thaat) == 10 def test_list_mela(): melas = librosa.list_mela() assert len(melas) == 72 for k in melas: assert 1 <= melas[k] <= 72 @pytest.mark.parametrize("thaat", librosa.list_thaat()) def test_thaat_to_degrees(thaat): degrees = librosa.thaat_to_degrees(thaat) assert len(degrees) == 7 assert np.all(degrees >= 0) and np.all(degrees < 12) @pytest.mark.parametrize("mela, idx", librosa.list_mela().items()) def test_mela_to_degrees(mela, idx): degrees = librosa.mela_to_degrees(mela) assert np.allclose(degrees, librosa.mela_to_degrees(idx)) assert len(degrees) == 7 assert np.all(degrees >= 0) and np.all(degrees < 12) if idx < 37: # check shuddha assert np.isin(5, degrees) else: # check prati assert np.isin(6, degrees) # Other checks?? @pytest.mark.parametrize( "mela, svara", # This list doesn't cover all 72, but it does cover the edge cases [ (1, ["S", "R�", "G�", "G₂", "G₃", "M�", "M₂", "P", "D�", "N�", "N₂", "N₃"]), (8, ["S", "R�", "R₂", "G₂", "G₃", "M�", "M₂", "P", "D�", "D₂", "N₂", "N₃"]), (15, ["S", "R�", "R₂", "G₂", "G₃", "M�", "M₂", "P", "D�", "D₂", "N₂", "N₃"]), (22, ["S", "R�", "R₂", "G₂", "G₃", "M�", "M₂", "P", "D�", "D₂", "N₂", "N₃"]), (29, ["S", "R�", "R₂", "G₂", "G₃", "M�", "M₂", "P", "D�", "D₂", "N₂", "N₃"]), (36, ["S", "R�", "R₂", "R₃", "G₃", "M�", "M₂", "P", "D�", "D₂", "D₃", "N₃"]), (43, ["S", "R�", "R₂", "G₂", "G₃", "M�", "M₂", "P", "D�", "N�", "N₂", "N₃"]), (50, ["S", "R�", "R₂", "G₂", "G₃", "M�", "M₂", "P", "D�", "D₂", "N₂", "N₃"]), (57, ["S", "R�", "R₂", "G₂", "G₃", "M�", "M₂", "P", "D�", "D₂", "N₂", "N₃"]), (64, ["S", "R�", "R₂", "G₂", "G₃", "M�", "M₂", "P", "D�", "D₂", "N₂", "N₃"]), (71, ["S", "R�", "R₂", "R₃", "G₃", "M�", "M₂", "P", "D�", "D₂", "N₂", "N₃"]), ], ) @pytest.mark.parametrize("abbr", [False, True]) @pytest.mark.parametrize("unicode", [False, True]) def test_mela_to_svara(mela, svara, abbr, unicode): svara_est = librosa.mela_to_svara(mela, abbr=abbr, unicode=unicode) for s1, s2 in zip(svara_est, svara): assert s1[0] == s2[0] if abbr: for s in svara_est: assert len(s) in (1, 2) else: for s in svara_est: assert 0 < len(s) < 5 if not unicode: # If we're in non-unicode mode, this shouldn't raise an exception for s in svara_est: assert s.isascii() @pytest.mark.xfail(raises=KeyError) def test_mela_to_degrees_badmela(): librosa.mela_to_degrees("some garbage") @pytest.mark.xfail(raises=librosa.ParameterError) def test_mela_to_degrees_badidx(): librosa.mela_to_degrees(0) @pytest.mark.xfail(raises=KeyError) def test_mela_to_svara_badmela(): librosa.mela_to_svara("some garbage") @pytest.mark.xfail(raises=librosa.ParameterError) def test_mela_to_svara_badidx(): librosa.mela_to_svara(0) @pytest.mark.parametrize( "unison, fifths, unicode, result", [ ("C", 0, True, "C"), ("C", 1, True, "G"), ("C", -2, True, "B♭"), ("C", -2, False, "Bb"), ("F", 1, True, "C"), ("F", -1, True, "B♭"), ("B", -7, True, "B♭"), ("Bb", 7, True, "B"), ("Bb", 14, True, "B♯"), ("B", 1, True, "F♯"), ("B", 14, True, "B�"), ("B", -14, True, "B�"), ("B", 21, True, "B�♯"), ("B", -21, True, "B�♭"), ("B", 21, False, "B###"), ("B", -21, False, "Bbbb"), ], ) def test_fifths_to_note(unison, fifths, unicode, result): note = librosa.core.notation.fifths_to_note( unison=unison, fifths=fifths, unicode=unicode ) assert note == result @pytest.mark.xfail(raises=librosa.ParameterError) def test_fifths_to_note_badunison(): librosa.fifths_to_note(unison="X", fifths=1) @pytest.mark.xfail(raises=librosa.ParameterError) def test_interval_to_fjs_irrational(): # Test FJS conversion with a non-just interval librosa.interval_to_fjs(np.sqrt(2)) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("r", [0, -1, -1 / 2]) def test_interval_to_fjs_nonpos(r): librosa.interval_to_fjs(r) @pytest.mark.parametrize( "interval, unison, unicode, result", [ (1, "C", True, "C"), (2, "G", True, "G"), (1 / 2, "F#", True, "F♯"), (1 / 2, "F#", False, "F#"), (3 / 2, "C", True, "G"), (5 / 4, "C", True, "E�"), (5 / 4, "C", False, "E^5"), (8 / 5, "E", True, "C₅"), (8 / 5, "E", False, "C_5"), (7 / 5, "F", True, "B�₅"), (7 / 5, "F", False, "B^7_5"), (49, "C", True, "G��"), (1 / 49, "C", True, "F₄₉"), ], ) def test_interval_to_fjs(interval, unison, unicode, result): note = librosa.interval_to_fjs(interval, unison=unison, unicode=unicode) assert note == result @pytest.mark.parametrize("unison", ["C", "F#", "Gbb"]) @pytest.mark.parametrize("unicode", [False, True]) @pytest.mark.parametrize( "intervals", [librosa.plimit_intervals(primes=[3, 5, 7], bins_per_octave=24)] ) def test_interval_to_fjs_set(unison, unicode, intervals): fjs = librosa.interval_to_fjs(intervals, unison=unison, unicode=unicode) for interval, note in zip(intervals, fjs): fjs_single = librosa.interval_to_fjs(interval, unison=unison, unicode=unicode) assert fjs_single == note @pytest.mark.parametrize( "hz, fmin, unison, unicode, results", [ ([55, 66, 77], None, None, True, ["A", "C₅", "D♯�₅"]), ([55, 66, 77], 33, None, True, ["A�", "C", "E♭�"]), ([55, 66, 77], 33, "Cb", True, ["A♭�", "C♭", "E��"]), ([55, 66, 77], 33, "Cb", False, ["Ab^5", "Cb", "Ebb^7"]), ], ) def test_hz_to_fjs(hz, fmin, unison, unicode, results): fjs = librosa.hz_to_fjs(hz, fmin=fmin, unison=unison, unicode=unicode) assert list(fjs) == results def test_hz_to_fjs_scalar(): fjs = librosa.hz_to_fjs(110, fmin=55, unicode=False) assert fjs == "A" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2332273 librosa-1.0.0/tests/test_onset.py0000644000175100017510000002635115236640653016535 0ustar00runnerrunner#!/usr/bin/env python # CREATED:2013-03-11 18:14:30 by Brian McFee # unit tests for librosa.onset import pytest from contextlib import nullcontext as dnr # Disable cache import os try: os.environ.pop("LIBROSA_CACHE_DIR") except KeyError: pass import warnings import numpy as np import librosa @pytest.fixture(scope="module") def ysr(): # Generate a pulse train at 120BPM sr = 22050 y = np.zeros(5 * sr) y[:: sr // 2] = 1 return y, sr @pytest.mark.parametrize( "feature", [None, librosa.feature.melspectrogram, librosa.feature.chroma_stft] ) @pytest.mark.parametrize("n_fft", [512, 2048]) @pytest.mark.parametrize("hop_length", [256, 512]) @pytest.mark.parametrize("center", [False, True]) @pytest.mark.parametrize("lag", [1]) @pytest.mark.parametrize("max_size", [1]) @pytest.mark.parametrize("aggregate", [np.mean]) @pytest.mark.parametrize("detrend", [False, True]) def test_onset_strength_audio( ysr, feature, n_fft, hop_length, center, lag, max_size, detrend, aggregate, ): # This test only really checks output shape and dimension # frame length, hop length, and centering matter # feature kind of matters # lag, max_size, detrending, and aggregation do not matter. y, sr = ysr oenv = librosa.onset.onset_strength( y=y, sr=sr, S=None, detrend=detrend, center=center, aggregate=aggregate, feature=feature, n_fft=n_fft, hop_length=hop_length, lag=lag, max_size=max_size, ) assert oenv.ndim == 1 # Calculate the number of frames that we should have for a centered analysis # Note that centering is enforced at the feature level, so the output shape # does not actually depend on the center parameter. # This may be something to change in the future. target_shape = 1 + len(y) // hop_length if not detrend: assert np.all(oenv >= 0) assert oenv.shape[-1] == target_shape def test_onset_strength_lag(melspec_sr): S, sr = melspec_sr oenv_skip = librosa.onset.onset_strength(S=S[..., ::2], sr=sr, lag=1) oenv_lag = librosa.onset.onset_strength(S=S, sr=sr, lag=2) # Lag of 2 should do the same thing at every other frame as # doing a decimated onset strength with lag=1. We do need # to be careful of boundary effects though, hence the edge slicing here. assert np.allclose(oenv_lag[:-2:2], oenv_skip[1:]) def test_onset_strength_max(melspec_sr): S, sr = melspec_sr oenv1 = librosa.onset.onset_strength(S=S, sr=sr, max_size=1) oenv2 = librosa.onset.onset_strength(S=S, sr=sr, max_size=3) # When we compare to the max over a larger set from the previous # frame, the novelty can only decrease. assert np.all(oenv1 >= oenv2) def test_onset_strength_aggregate(melspec_sr): S, sr = melspec_sr oenv_mean = librosa.onset.onset_strength(S=S, sr=sr, aggregate=np.mean) oenv_max = librosa.onset.onset_strength(S=S, sr=sr, aggregate=np.max) oenv_min = librosa.onset.onset_strength(S=S, sr=sr, aggregate=np.min) assert np.all(oenv_mean >= oenv_min) assert np.all(oenv_mean <= oenv_max) @pytest.mark.xfail(raises=librosa.ParameterError) def test_onset_strength_badlag(ysr): y, sr = ysr librosa.onset.onset_strength(y=y, sr=sr, lag=0) @pytest.mark.xfail(raises=librosa.ParameterError) def test_onset_strength_badmax(ysr): y, sr = ysr librosa.onset.onset_strength(y=y, sr=sr, max_size=0) @pytest.mark.xfail(raises=librosa.ParameterError) def test_onset_strength_noinput(): librosa.onset.onset_strength(y=None, S=None) @pytest.fixture(scope="module") def melspec_sr(ysr): y, sr = ysr S = librosa.feature.melspectrogram(y=y, sr=sr) return S, sr @pytest.mark.parametrize("feature", [None, librosa.feature.chroma_stft]) @pytest.mark.parametrize("n_fft", [512, 2048]) @pytest.mark.parametrize("hop_length", [256, 512]) @pytest.mark.parametrize("detrend", [False, True]) @pytest.mark.parametrize("center", [False, True]) @pytest.mark.parametrize("aggregate", [None, np.max]) def test_onset_strength_spectrogram( melspec_sr, feature, n_fft, hop_length, detrend, center, aggregate ): S, sr = melspec_sr oenv = librosa.onset.onset_strength( y=None, sr=sr, S=S, detrend=detrend, center=center, aggregate=aggregate, feature=feature, n_fft=n_fft, hop_length=hop_length, ) assert oenv.ndim == 1 target_shape = S.shape[-1] if not detrend: assert np.all(oenv >= 0) assert oenv.shape[-1] == target_shape @pytest.mark.parametrize("lag", [1, 2, 3]) @pytest.mark.parametrize("aggregate", [np.mean, np.max]) def test_onset_strength_multi_noagg(melspec_sr, lag, aggregate): S, sr = melspec_sr # We only test with max_size=1 here to make the sub-band slicing test simple odf_multi = librosa.onset.onset_strength_multi( S=S, lag=lag, max_size=1, aggregate=False ) odf_mean = librosa.onset.onset_strength_multi( S=S, lag=lag, max_size=1, aggregate=aggregate ) # With no aggregation, output shape should = input shape assert odf_multi.shape == S.shape # Result should average out to the same as mean aggregation assert np.allclose(odf_mean, aggregate(odf_multi, axis=0)) @pytest.fixture(scope="module") def channels(melspec_sr): S, _ = melspec_sr return np.linspace(0, S.shape[0], num=5, dtype=int) @pytest.mark.parametrize("lag", [1, 2, 3]) def test_onset_strength_multi(melspec_sr, lag, channels): S, sr = melspec_sr # We only test with max_size=1 here to make the sub-band slicing test simple odf_multi = librosa.onset.onset_strength_multi( S=S, lag=lag, max_size=1, channels=channels ) assert len(odf_multi) == len(channels) - 1 for i, (s, t) in enumerate(zip(channels, channels[1:])): odf_single = librosa.onset.onset_strength(S=S[s:t], lag=lag, max_size=1) assert np.allclose(odf_single, odf_multi[i]) @pytest.fixture(scope="module", params=[64, 512, 2048]) def hop(request): return request.param @pytest.fixture(scope="module", params=[False, True], ids=["audio", "oenv"]) def oenv(ysr, hop, request): if request.param: y, sr = ysr return librosa.onset.onset_strength(y=y, sr=sr, hop_length=hop) else: return None @pytest.mark.parametrize("bt", [False, True]) @pytest.mark.parametrize("normalize", [False, True]) def test_onset_detect_real(ysr, oenv, hop, bt, normalize): y, sr = ysr onsets = librosa.onset.onset_detect( y=y, sr=sr, onset_envelope=oenv, hop_length=hop, backtrack=bt, normalize=normalize, ) if bt: assert np.all(onsets >= 0) else: assert np.all(onsets > 0) assert np.all(onsets < len(y) * sr // hop) if oenv is not None: assert np.all(onsets < len(oenv)) @pytest.mark.xfail(raises=librosa.ParameterError) def test_onset_detect_nosignal(): librosa.onset.onset_detect(y=None, onset_envelope=None) @pytest.mark.parametrize("sr", [4000]) @pytest.mark.parametrize("y", [np.zeros(4000), np.ones(4000), -np.ones(4000)]) @pytest.mark.parametrize("hop_length", [64, 512, 2048]) def test_onset_detect_const(y, sr, hop_length): # Disable padding here onsets = librosa.onset.onset_detect( y=y, sr=sr, onset_envelope=None, hop_length=hop_length, ) # We'll allow one onset at the start of the signal for these examples # when y is all-ones, zero-padding induces an onset at the beginning of the # signal assert len(onsets) == 0 or (y[0] != 0 and len(onsets) == 1) def test_onset_detect_dense_const(): # Make an empty onset strength envelope z = np.zeros(512) onsets = librosa.onset.onset_detect(onset_envelope=z, sparse=False) assert z.shape == onsets.shape assert onsets.dtype is np.dtype(bool) assert not np.any(onsets) @pytest.mark.parametrize( "units, ctx", [ ("frames", dnr()), ("time", dnr()), ("samples", dnr()), ("bad units", pytest.raises(librosa.ParameterError)), ], ) @pytest.mark.parametrize("hop_length", [512, 1024]) def test_onset_units(ysr, hop_length, units, ctx): y, sr = ysr with ctx: b1 = librosa.onset.onset_detect(y=y, sr=sr, hop_length=hop_length) b2 = librosa.onset.onset_detect(y=y, sr=sr, hop_length=hop_length, units=units) t1 = librosa.frames_to_time(b1, sr=sr, hop_length=hop_length) if units == "time": t2 = b2 elif units == "samples": t2 = librosa.samples_to_time(b2, sr=sr) elif units == "frames": t2 = librosa.frames_to_time(b2, sr=sr, hop_length=hop_length) else: assert False assert np.allclose(t1, t2) @pytest.fixture(scope="module", params=[False, True], ids=["oenv", "rms"]) def energy(ysr, hop, request): y, sr = ysr if request.param: return librosa.onset.onset_strength(y=y, sr=sr, hop_length=hop) else: return librosa.feature.rms(y=y, hop_length=hop) def test_onset_backtrack(ysr, oenv, hop, energy): y, sr = ysr onsets = librosa.onset.onset_detect( y=y, sr=sr, onset_envelope=oenv, hop_length=hop, backtrack=False ) # Test backtracking onsets_bt = librosa.onset.onset_backtrack(onsets, energy) # Make sure there are no negatives assert np.all(onsets_bt >= 0) # And that we never roll forward assert np.all(onsets_bt <= onsets) # And that the detected peaks are actually minima assert np.all(energy[onsets_bt] <= energy[np.maximum(0, onsets_bt - 1)]) @pytest.mark.xfail(raises=librosa.ParameterError) def test_onset_backtrack_fail(ysr): y, sr = ysr onsets = librosa.onset.onset_detect(y=y, sr=sr, backtrack=True, sparse=False) def test_onset_sparse(ysr, oenv, hop, energy): y, sr = ysr onsets = librosa.onset.onset_detect( y=y, sr=sr, onset_envelope=oenv, hop_length=hop, sparse=True ) onsetsd = librosa.onset.onset_detect( y=y, sr=sr, onset_envelope=oenv, hop_length=hop, sparse=False ) assert np.allclose(onsets, np.flatnonzero(onsetsd)) @pytest.mark.xfail(raises=librosa.ParameterError) def test_onset_strength_noagg(): S = np.zeros((3, 3)) librosa.onset.onset_strength(S=S, aggregate=False) @pytest.mark.xfail(raises=librosa.ParameterError) def test_onset_strength_badref(): S = np.zeros((3, 3)) librosa.onset.onset_strength(S=S, ref=S[:, :2]) def test_onset_strength_multi_ref(rng): # Make a random positive spectrum S = 1 + np.abs(rng.standard_normal(size=(1025, 10))) # Test with a null reference null_ref = np.zeros_like(S) onsets = librosa.onset.onset_strength_multi( S=S, ref=null_ref, aggregate=False, center=False ) # since the reference is zero everywhere, S - ref = S # past the setup phase (first frame) assert np.allclose(onsets[:, 1:], S[:, 1:]) def test_onset_detect_inplace_normalize(): # This test will fail if the in-place normalization modifies # the input onset envelope oenv_in = np.ones(50) oenv_in[10] = 2 oenv_orig = oenv_in.copy() librosa.onset.onset_detect(onset_envelope=oenv_in, normalize=True) assert np.allclose(oenv_in, oenv_orig) and oenv_in is not oenv_orig ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2332273 librosa-1.0.0/tests/test_segment.py0000644000175100017510000004244115236640653017045 0ustar00runnerrunner#!/usr/bin/env python # -*- encoding: utf-8 -*- """Tests for segmentation functions""" from typing import TYPE_CHECKING, Union # Disable cache import os try: os.environ.pop("LIBROSA_CACHE_DIR") except KeyError: pass import numpy as np import scipy from scipy.spatial.distance import cdist, pdist, squareform import pytest from typing import cast import librosa from librosa._typing import _SparseArray @pytest.mark.parametrize("n", [20, 250]) @pytest.mark.parametrize("k", [None, 5]) @pytest.mark.parametrize("metric", ["l2", "cosine"]) def test_cross_similarity(n, k, metric, rng): # Make a data matrix data_ref = rng.standard_normal(size=(3, n)) data = rng.standard_normal(size=(3, n + 7)) D = librosa.segment.cross_similarity(data, data_ref, k=k, metric=metric) assert D.shape == (data_ref.shape[1], data.shape[1]) if k is not None: real_k = min(k, n) assert not np.any(D.sum(axis=0) != real_k) def test_cross_similarity_sparse(rng): data_ref = rng.standard_normal(size=(3, 50)) data = rng.standard_normal(size=(3, 100)) D_sparse = librosa.segment.cross_similarity(data, data_ref, sparse=True) D_dense = librosa.segment.cross_similarity(data, data_ref, sparse=False) assert scipy.sparse.issparse(D_sparse) assert isinstance(D_sparse, scipy.sparse.sparray) assert not isinstance(D_sparse, scipy.sparse.spmatrix) assert np.allclose(D_sparse.toarray(), D_dense) def test_cross_similarity_distance(rng): data_ref = rng.standard_normal(size=(3, 50)) data = rng.standard_normal(size=(3, 70)) distance = cdist(data.T, data_ref.T, metric="sqeuclidean").T rec = librosa.segment.cross_similarity( data, data_ref, mode="distance", metric="sqeuclidean", sparse=True ) assert isinstance(rec, scipy.sparse.sparray) assert not isinstance(rec, scipy.sparse.spmatrix) i, j, vals = scipy.sparse.find(rec) assert np.allclose(vals, distance[i, j]) @pytest.mark.parametrize("metric", ["sqeuclidean", "cityblock"]) @pytest.mark.parametrize("bandwidth", [None, 1]) def test_cross_similarity_affinity(metric, bandwidth, rng): data_ref = rng.standard_normal(size=(3, 70)) data = rng.standard_normal(size=(3, 50)) distance = cdist(data_ref.T, data.T, metric=metric) rec = librosa.segment.cross_similarity( data, data_ref, mode="affinity", metric=metric, sparse=True, bandwidth=bandwidth ) assert isinstance(rec, scipy.sparse.sparray) assert not isinstance(rec, scipy.sparse.spmatrix) i, j, vals = scipy.sparse.find(rec) logvals = np.log(vals) ratio = -logvals / (distance[i, j] + librosa.util.tiny(distance)) if bandwidth is None: assert np.allclose(-logvals, distance[i, j] * np.nanmax(ratio)) else: assert np.allclose(-logvals, distance[i, j] * bandwidth) def test_cross_similarity_full(): data = np.eye(10) data_ref = np.eye(10) rec = librosa.segment.cross_similarity(data, data_ref, mode="distance", full=True) assert np.all(rec >= 0) @pytest.mark.xfail(raises=librosa.ParameterError) def test_cross_similarity_badmode(rng): data_ref = rng.standard_normal(size=(3, 70)) data = rng.standard_normal(size=(3, 50)) rec = librosa.segment.cross_similarity( data, data_ref, mode="NOT A MODE", metric="sqeuclidean", sparse=True ) @pytest.mark.xfail(raises=librosa.ParameterError) def test_cross_similarity_bad_bandwidth(rng): data_ref = rng.standard_normal(size=(3, 50)) data = rng.standard_normal(size=(3, 70)) rec = librosa.segment.cross_similarity( data, data_ref, bandwidth=-2, mode="affinity" ) @pytest.mark.xfail(raises=librosa.ParameterError) def test_cross_similarity_fail_mismatch(): D1 = np.zeros((3, 3)) D2 = np.zeros((2, 3)) librosa.segment.cross_similarity(D1, D2) def test_cross_similarity_multi(rng): X1 = rng.standard_normal(size=(2, 10, 100)) X2 = rng.standard_normal(size=(2, 10, 50)) R = librosa.segment.cross_similarity(X1, X2, mode="affinity") # This should give the same output as if we stacked out the leading channel X1f = np.concatenate([X1[0], X1[1]], axis=0) X2f = np.concatenate([X2[0], X2[1]], axis=0) Rf = librosa.segment.cross_similarity(X1f, X2f, mode="affinity") assert np.allclose(R, Rf) @pytest.mark.parametrize("n", [20, 40]) @pytest.mark.parametrize("k", [None, 5]) @pytest.mark.parametrize("sym", [False, True]) @pytest.mark.parametrize("width", [1, 5]) @pytest.mark.parametrize("metric", ["l2", "cosine"]) @pytest.mark.parametrize("self", [False, True]) def test_recurrence_matrix(n, k, width, sym, metric, self, rng): # Make a data matrix data = rng.standard_normal(size=(3, n)) D = librosa.segment.recurrence_matrix( data, k=k, width=width, sym=sym, axis=-1, metric=metric, self=self ) # First test for symmetry if sym: assert np.allclose(D, D.T) # Test for target-axis invariance D_trans = librosa.segment.recurrence_matrix( data.T, k=k, width=width, sym=sym, axis=0, metric=metric, self=self ) assert np.allclose(D, D_trans) # If not symmetric, test for correct number of links if not sym and k is not None: real_k = min(k, n - width) if self: real_k += 1 assert not np.any(D.sum(axis=0) != real_k) if self: assert np.allclose(np.diag(D), True) # Make sure the +- width diagonal is hollow # It's easier to test if zeroing out the triangles leaves nothing idx = np.tril_indices(n, k=width) D[idx] = False D.T[idx] = False assert not np.any(D) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("data", [np.ones((3, 10))]) @pytest.mark.parametrize("width", [-1, 0, 11]) def test_recurrence_badwidth(data, width): librosa.segment.recurrence_matrix(data, width=width) @pytest.mark.parametrize("self", [False, True]) def test_recurrence_sparse(self, rng): data = rng.standard_normal(size=(3, 100)) D_sparse = librosa.segment.recurrence_matrix(data, sparse=True, self=self) D_dense = librosa.segment.recurrence_matrix(data, sparse=False, self=self) assert scipy.sparse.issparse(D_sparse) assert isinstance(D_sparse, scipy.sparse.sparray) assert not isinstance(D_sparse, scipy.sparse.spmatrix) assert np.allclose(D_sparse.toarray(), D_dense) if self: assert np.allclose(D_sparse.diagonal(), True) else: assert np.allclose(D_sparse.diagonal(), False) @pytest.mark.parametrize("self", [False, True]) def test_recurrence_distance(self, rng): data = rng.standard_normal(size=(3, 100)) distance = squareform(pdist(data.T, metric="sqeuclidean")) rec = librosa.segment.recurrence_matrix( data, mode="distance", metric="sqeuclidean", sparse=True, self=self ) assert isinstance(rec, scipy.sparse.sparray) assert not isinstance(rec, scipy.sparse.spmatrix) i, j, vals = scipy.sparse.find(rec) assert np.allclose(vals, distance[i, j]) assert np.allclose(rec.diagonal(), 0.0) @pytest.mark.parametrize("metric", ["sqeuclidean", "cityblock"]) @pytest.mark.parametrize("bandwidth", [None, 1]) @pytest.mark.parametrize("self", [False, True]) def test_recurrence_affinity(metric, bandwidth, self, rng): data = rng.standard_normal(size=(3, 100)) distance = squareform(pdist(data.T, metric=metric)) rec = librosa.segment.recurrence_matrix( data, mode="affinity", metric=metric, sparse=True, bandwidth=bandwidth, self=self, ) assert isinstance(rec, scipy.sparse.sparray) assert not isinstance(rec, scipy.sparse.spmatrix) if self: assert np.allclose(rec.diagonal(), 1.0) else: assert np.allclose(rec.diagonal(), 0.0) i, j, vals = scipy.sparse.find(rec) logvals = np.log(vals) # After log-scaling, affinity will match distance up to a constant factor ratio = -logvals / (distance[i, j] + librosa.util.tiny(distance)) if bandwidth is None: # Estimate the global bandwidth using non-zero distances assert np.allclose(-logvals, distance[i, j] * np.nanmax(ratio)) else: assert np.allclose(-logvals, distance[i, j] * bandwidth) def test_recurrence_full(): data = np.eye(10) rec = librosa.segment.recurrence_matrix( data, mode="distance", metric="euclidean", sparse=False, full=True ) assert np.all(rec >= 0) @pytest.mark.xfail(raises=librosa.ParameterError) def test_big_width(rng): data = rng.standard_normal(size=(3, 100)) width = 55 auto_k_rec = librosa.segment.recurrence_matrix(data, mode="affinity", width=width) @pytest.mark.xfail(raises=librosa.ParameterError) def test_empty_data_recurrence(): data = np.zeros((10, 10)) librosa.segment.recurrence_matrix(data, mode="affinity") @pytest.mark.xfail(raises=librosa.ParameterError) def test_empty_rows_recurrence(): data = np.zeros((10, 10)) data[0, 5] = 1 librosa.segment.recurrence_matrix(data, mode="affinity", bandwidth="mean_k") def test_empty_rows_recurrence_okay(): data = np.zeros((10, 10)) data[0, 5] = 1 librosa.segment.recurrence_matrix(data, mode="affinity", bandwidth="med_k_scalar") def test_recurrence_multi(rng): X = rng.standard_normal(size=(2, 10, 100)) R = librosa.segment.recurrence_matrix(X, mode="affinity") # This should give the same output as if we stacked out the leading channel Xf = np.concatenate([X[0], X[1]], axis=0) Rf = librosa.segment.recurrence_matrix(Xf, mode="affinity") assert np.allclose(R, Rf) @pytest.mark.xfail(raises=librosa.ParameterError) def test_recurrence_badmode(rng): data = rng.standard_normal(size=(3, 100)) rec = librosa.segment.recurrence_matrix( data, mode="NOT A MODE", metric="sqeuclidean", sparse=True ) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize( "bandwidth", [-2, "FAKE", np.ones((2, 5)), -1 * np.ones((100, 100))] ) def test_recurrence_bad_bandwidth(bandwidth, rng): data = rng.standard_normal(size=(3, 100)) rec = librosa.segment.recurrence_matrix(data, bandwidth=bandwidth, mode="affinity") def test_recurrence_array_bandwidth(rng): data = rng.standard_normal(size=(3, 100)) bw = rng.random((100, 100)) + 0.1 rec = librosa.segment.recurrence_matrix(data, bandwidth=bw, mode="affinity") @pytest.mark.parametrize( "bw_mode", ["mean_k", "gmean_k", "mean_k_avg", "gmean_k_avg", "mean_k_avg_and_pair"] ) def test_automatic_bandwidth(bw_mode, rng): data = rng.standard_normal(size=(3, 100)) rec = librosa.segment.recurrence_matrix(data, bandwidth=bw_mode, mode="affinity") @pytest.mark.parametrize("n", [10, 100, 500]) @pytest.mark.parametrize("pad", [False, True]) def test_recurrence_to_lag(n, pad, rng): data = rng.standard_normal(size=(17, n)) rec = librosa.segment.recurrence_matrix(data) lag = librosa.segment.recurrence_to_lag(rec, pad=pad, axis=-1) lag2 = librosa.segment.recurrence_to_lag(rec.T, pad=pad, axis=0) assert np.allclose(lag, lag2.T) x: Union[ellipsis, slice] = Ellipsis if pad: x = slice(n) for i in range(n): assert np.allclose(rec[:, i], np.roll(lag[:, i], i)[x]) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("size", [(17,), (17, 34), (17, 17, 17)]) def test_recurrence_to_lag_fail(size): librosa.segment.recurrence_to_lag(np.zeros(size)) @pytest.mark.parametrize("pad", [False, True]) @pytest.mark.parametrize("axis", [0, 1, -1]) @pytest.mark.parametrize( "rec", [librosa.segment.recurrence_matrix(np.ones((3, 100)), sparse=True)] ) @pytest.mark.parametrize("fmt", ["csc", "csr", "lil", "bsr", "dia"]) # This warning is expected when using fmt='dia' @pytest.mark.filterwarnings("ignore:Constructing a DIA matrix") def test_recurrence_to_lag_sparse(pad, axis, rec, fmt): rec_dense = rec.toarray() rec = rec.asformat(fmt) lag_sparse = librosa.segment.recurrence_to_lag(rec, pad=pad, axis=axis) lag_dense = librosa.segment.recurrence_to_lag(rec_dense, pad=pad, axis=axis) assert scipy.sparse.issparse(lag_sparse) assert isinstance(lag_sparse, scipy.sparse.sparray) assert not isinstance(lag_sparse, scipy.sparse.spmatrix) lag_sparse = cast(_SparseArray, lag_sparse) assert rec.format == lag_sparse.format assert rec.dtype == lag_sparse.dtype assert np.allclose(lag_sparse.toarray(), lag_dense) @pytest.mark.parametrize("n", [10, 100]) @pytest.mark.parametrize("pad", [False, True]) def test_lag_to_recurrence(n, pad, rng): data = rng.standard_normal(size=(17, n)) rec = librosa.segment.recurrence_matrix(data) lag = librosa.segment.recurrence_to_lag(rec, pad=pad, axis=-1) lag2 = librosa.segment.recurrence_to_lag(rec.T, pad=pad, axis=0).T rec2 = librosa.segment.lag_to_recurrence(lag) assert np.allclose(rec, rec2) assert np.allclose(lag, lag2) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("size", [(17,), (17, 35), (17, 17, 17)]) def test_lag_to_recurrence_badsize(size): librosa.segment.lag_to_recurrence(np.zeros(size)) @pytest.mark.parametrize("axis", [0, 1, -1]) @pytest.mark.parametrize("pad", [False, True]) def test_lag_to_recurrence_sparse(axis, pad, rng): data = rng.standard_normal(size=(3, 10)) rec = librosa.segment.recurrence_matrix(data, sparse=True) lag = librosa.segment.recurrence_to_lag(rec, pad=pad, axis=axis) lag_dense = lag.toarray() rec_sparse = librosa.segment.lag_to_recurrence(lag, axis=axis) rec_dense = librosa.segment.lag_to_recurrence(lag_dense, axis=axis) assert scipy.sparse.issparse(rec_sparse) assert isinstance(rec_sparse, scipy.sparse.sparray) assert not isinstance(rec_sparse, scipy.sparse.spmatrix) assert rec_sparse.format == lag.format assert rec_sparse.dtype == lag.dtype assert np.allclose(rec_sparse.toarray(), rec_dense) @pytest.mark.xfail(raises=librosa.ParameterError) def test_lag_to_recurrence_sparse_badaxis(rng): data = rng.standard_normal(size=(3, 100)) R = librosa.segment.recurrence_matrix(data, sparse=True) L = librosa.segment.recurrence_to_lag(R) librosa.segment.lag_to_recurrence(L, axis=2) def test_timelag_filter(rng): X = rng.standard_normal(size=(15, 15)) d_pos0 = librosa.segment.timelag_filter(lambda X: X) assert np.allclose(X, d_pos0(X)) def test_timelag_filter_pos1(rng): X = rng.standard_normal(size=(15, 15)) d_pos1 = librosa.segment.timelag_filter(lambda _, X: X, index=1) assert np.allclose(X, d_pos1(None, X)) @pytest.fixture(scope="module") def ysr(): sr = 22050 y = librosa.chirp(fmin=100, fmax=2000, sr=sr, duration=5.0) return y, sr @pytest.fixture(scope="module") def mfcc(ysr): y, sr = ysr return librosa.feature.mfcc(y=y, sr=sr) @pytest.fixture(scope="module") def beats(ysr): y, sr = ysr tempo, beats = librosa.beat.beat_track(y=y, sr=sr) return beats @pytest.mark.parametrize("n_segments", [1, 2, 3, 4, 100]) def test_subsegment(mfcc, beats, n_segments): subseg = librosa.segment.subsegment(mfcc, beats, n_segments=n_segments, axis=-1) # Make sure that the boundaries are within range assert subseg.min() >= 0 assert subseg.max() <= mfcc.shape[-1] # Make sure that all input beats are retained for b in beats: assert b in subseg # Do we have a 0 marker? assert 0 in subseg # Did we over-segment? +2 here for 0- and end-padding assert len(subseg) <= n_segments * (len(beats) + 2) # Verify that running on the transpose gives the same answer ss2 = librosa.segment.subsegment(mfcc.T, beats, n_segments=n_segments, axis=0) assert np.allclose(subseg, ss2) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("n_segments", [-1, 0]) def test_subsegment_badn(mfcc, beats, n_segments): librosa.segment.subsegment(mfcc, beats, n_segments=n_segments, axis=-1) @pytest.fixture def R_input(rng): X = rng.standard_normal(size=(30, 5)) return X.dot(X.T) @pytest.mark.parametrize("window", ["rect", "hann"]) @pytest.mark.parametrize("n", [5, 9]) @pytest.mark.parametrize("max_ratio", [1.0, 1.5, 2.0]) @pytest.mark.parametrize("min_ratio", [None, 1.0]) @pytest.mark.parametrize("n_filters", [1, 2, 5]) @pytest.mark.parametrize("zero_mean", [False, True]) @pytest.mark.parametrize("clip", [False, True]) @pytest.mark.parametrize("kwargs", [dict(), dict(mode="reflect")]) def test_path_enhance( R_input, window, n, max_ratio, min_ratio, n_filters, zero_mean, clip, kwargs ): R_smooth = librosa.segment.path_enhance( R_input, window=window, n=n, max_ratio=max_ratio, min_ratio=min_ratio, n_filters=n_filters, zero_mean=zero_mean, clip=clip, **kwargs, ) assert R_smooth.shape == R_input.shape assert np.all(np.isfinite(R_smooth)) assert R_smooth.dtype == R_input.dtype if clip: assert np.min(R_smooth) >= 0 @pytest.mark.xfail(raises=librosa.ParameterError) def test_path_enhance_badratio(R_input): # We can't have min_ratio > max_ratio librosa.segment.path_enhance(R_input, n=5, min_ratio=3, max_ratio=2) def test_path_enhance_multi(rng): R = rng.standard_normal(size=(2, 100, 100)) Rs0 = librosa.segment.path_enhance(R[0], n=5) Rs1 = librosa.segment.path_enhance(R[1], n=5) Rs = librosa.segment.path_enhance(R, n=5) assert np.allclose(Rs0, Rs[0]) assert np.allclose(Rs1, Rs[1]) assert not np.allclose(Rs0, Rs1) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2332273 librosa-1.0.0/tests/test_sequence.py0000644000175100017510000006424115236640653017215 0ustar00runnerrunner#!/usr/bin/env python # -*- encoding: utf-8 -*- import numpy as np import pytest import librosa # Core viterbi tests def test_viterbi_example(): # Example from https://en.wikipedia.org/wiki/Viterbi_algorithm#Example # States: 0 = healthy, 1 = fever p_init = np.asarray([0.6, 0.4]) # state 0 = hi, state 1 = low transition = np.asarray([[0.7, 0.3], [0.4, 0.6]]) # emission likelihoods emit_p = [ dict(normal=0.5, cold=0.4, dizzy=0.1), dict(normal=0.1, cold=0.3, dizzy=0.6), ] obs = ["normal", "cold", "dizzy"] prob = np.asarray([np.asarray([ep[o] for o in obs]) for ep in emit_p]) path, logp = librosa.sequence.viterbi( prob, transition, p_init=p_init, return_logp=True ) # True maximum likelihood state assert np.array_equal(path, [0, 0, 1]) assert np.isclose(logp, np.log(0.01512)) # And check the second execution path path2 = librosa.sequence.viterbi(prob, transition, p_init=p_init, return_logp=False) assert np.array_equal(path, path2) def test_viterbi_multichannel(): # Example from https://en.wikipedia.org/wiki/Viterbi_algorithm#Example # States: 0 = healthy, 1 = fever p_init = np.asarray([0.6, 0.4]) # state 0 = hi, state 1 = low transition = np.asarray([[0.7, 0.3], [0.4, 0.6]]) # emission likelihoods emit_p = [ dict(normal=0.5, cold=0.4, dizzy=0.1), dict(normal=0.1, cold=0.3, dizzy=0.6), ] obs = ["normal", "cold", "dizzy"] prob = np.asarray([np.asarray([ep[o] for o in obs]) for ep in emit_p]) # Make a 3-channel stack prob_mc = np.stack([prob, 1 - prob, prob[:, ::-1]]) path, logp = librosa.sequence.viterbi( prob_mc, transition, p_init=p_init, return_logp=True ) # Check the second execution path path2 = librosa.sequence.viterbi( prob_mc, transition, p_init=p_init, return_logp=False ) assert np.array_equal(path, path2) # Check each individual path path0, logp0 = librosa.sequence.viterbi( prob_mc[0], transition, p_init=p_init, return_logp=True ) assert np.allclose(path0, path[0]) assert np.allclose(logp0, logp[0]) path1, logp1 = librosa.sequence.viterbi( prob_mc[1], transition, p_init=p_init, return_logp=True ) assert np.allclose(path1, path[1]) assert np.allclose(logp1, logp[1]) path2, logp2 = librosa.sequence.viterbi( prob_mc[2], transition, p_init=p_init, return_logp=True ) assert np.allclose(path2, path[2]) assert np.allclose(logp2, logp[2]) @pytest.mark.parametrize("transition_min_prob", [None, 0, 1e-10, 0.25]) def test_viterbi_sparse(transition_min_prob): p_init = np.asarray([0.6, 0.4]) transition = np.asarray([[0.7, 0.3], [0.4, 0.6]]) emit_p = [ dict(normal=0.5, cold=0.4, dizzy=0.1), dict(normal=0.1, cold=0.3, dizzy=0.6), ] obs = ["normal", "cold", "dizzy"] prob = np.asarray([np.asarray([ep[o] for o in obs]) for ep in emit_p]) path, logp = librosa.sequence.viterbi( prob, transition, p_init=p_init, return_logp=True, transition_min_prob=transition_min_prob ) ref_path, reg_logp = librosa.sequence.viterbi( prob, transition, p_init=p_init, return_logp=True, transition_min_prob=None ) assert np.array_equal(path, ref_path) assert np.isclose(logp, reg_logp) @pytest.mark.xfail(raises=librosa.ParameterError) def test_viterbi_negative_minprob(): p_init = np.asarray([0.6, 0.4]) transition = np.asarray([[0.7, 0.3], [0.4, 0.6]]) emit_p = [ dict(normal=0.5, cold=0.4, dizzy=0.1), dict(normal=0.1, cold=0.3, dizzy=0.6), ] obs = ["normal", "cold", "dizzy"] prob = np.asarray([np.asarray([ep[o] for o in obs]) for ep in emit_p]) librosa.sequence.viterbi( prob, transition, p_init=p_init, return_logp=True, transition_min_prob=-1 ) @pytest.mark.xfail(raises=librosa.ParameterError) def test_viterbi_discriminative_negative_minprob(): transition = np.asarray([[0.75, 0.25], [0.25, 0.75]]) p_joint = np.asarray([[0.25, 0.25], [0.1, 0.4]]) p_obs_marginal = p_joint.sum(axis=0) p_state_marginal = p_joint.sum(axis=1) p_init = p_state_marginal p_state_given_obs = (p_joint / p_obs_marginal).T seq = np.asarray([1, 1, 0, 1, 1, 1, 0, 0]) prob_d = np.asarray([p_state_given_obs[i] for i in seq]).T librosa.sequence.viterbi_discriminative( prob_d, transition, p_state=p_state_marginal, p_init=p_init, return_logp=True, transition_min_prob=-1 ) @pytest.mark.xfail(raises=librosa.ParameterError) def test_viterbi_sparse_fail(): p_init = np.asarray([0.6, 0.4]) transition = np.asarray([[0.7, 0.3], [0.4, 0.6]]) emit_p = [ dict(normal=0.5, cold=0.4, dizzy=0.1), dict(normal=0.1, cold=0.3, dizzy=0.6), ] obs = ["normal", "cold", "dizzy"] prob = np.asarray([np.asarray([ep[o] for o in obs]) for ep in emit_p]) # This threshold is above all transition probabilities into state 1 # so the algorithm should raise an error about disconnected states librosa.sequence.viterbi( prob, transition, p_init=p_init, return_logp=True, transition_min_prob=0.65 ) def test_viterbi_init(): # Example from https://en.wikipedia.org/wiki/Viterbi_algorithm#Example # States: 0 = healthy, 1 = fever p_init = np.asarray([0.5, 0.5]) # state 0 = hi, state 1 = low transition = np.asarray([[0.7, 0.3], [0.4, 0.6]]) # emission likelihoods emit_p = [ dict(normal=0.5, cold=0.4, dizzy=0.1), dict(normal=0.1, cold=0.3, dizzy=0.6), ] obs = ["normal", "cold", "dizzy"] prob = np.asarray([np.asarray([ep[o] for o in obs]) for ep in emit_p]) path1, logp1 = librosa.sequence.viterbi( prob, transition, p_init=p_init, return_logp=True ) path2, logp2 = librosa.sequence.viterbi(prob, transition, return_logp=True) assert np.array_equal(path1, path2) assert logp1 == logp2 @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize( "trans", [ np.ones((3, 3), dtype=float), np.ones((3, 2), dtype=float), np.ones((2, 2), dtype=float), np.asarray([[1, 1, -1], [1, 1, -1], [1, 1, -1]], dtype=float), ], ids=["sum!=1", "not square", "too small", "negative"], ) def test_viterbi_bad_transition(trans, rng): x = rng.random(size=(3, 5)) librosa.sequence.viterbi(x, trans) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("trans", [np.ones((3, 3), dtype=float) / 3.0]) @pytest.mark.parametrize( "p_init", [ np.ones(3, dtype=float), np.ones(4, dtype=float) / 4.0, np.asarray([1, 1, -1], dtype=float), ], ids=["sum!=1", "wrong size", "negative"], ) def test_viterbi_bad_init(trans, p_init, rng): x = rng.random(size=(3, 5)) librosa.sequence.viterbi(x, trans, p_init=p_init) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("trans", [np.ones((3, 3), dtype=float) / 3]) @pytest.mark.parametrize( "offset", [2, -1], ids=["p>1", "p<0"], ) def test_viterbi_bad_obs(trans, offset, rng): x = rng.random(size=(3, 5)) + offset librosa.sequence.viterbi(x, trans) # Discriminative viterbi @pytest.mark.parametrize("transition_min_prob", [None, 1e-3]) def test_viterbi_discriminative_example(transition_min_prob): # A pre-baked example with coin tosses transition = np.asarray([[0.75, 0.25], [0.25, 0.75]]) # Joint XY model p_joint = np.asarray([[0.25, 0.25], [0.1, 0.4]]) # marginals p_obs_marginal = p_joint.sum(axis=0) p_state_marginal = p_joint.sum(axis=1) p_init = p_state_marginal # Make the Y|X distribution p_state_given_obs = (p_joint / p_obs_marginal).T # Let's make a test observation sequence seq = np.asarray([1, 1, 0, 1, 1, 1, 0, 0]) # Then our conditional probability table can be constructed directly as prob_d = np.asarray([p_state_given_obs[i] for i in seq]).T path, logp = librosa.sequence.viterbi_discriminative( prob_d, transition, p_state=p_state_marginal, p_init=p_init, return_logp=True, transition_min_prob=transition_min_prob ) # Pre-computed optimal path, determined by brute-force search assert np.array_equal(path, [1, 1, 1, 1, 1, 1, 0, 0]) # And check the second code path path2 = librosa.sequence.viterbi_discriminative( prob_d, transition, p_state=p_state_marginal, p_init=p_init, return_logp=False, transition_min_prob=transition_min_prob ) assert np.array_equal(path, path2) def test_viterbi_discriminative_multi(): # A pre-baked example with coin tosses transition = np.asarray([[0.75, 0.25], [0.25, 0.75]]) # Joint XY model p_joint = np.asarray([[0.25, 0.25], [0.1, 0.4]]) # marginals p_obs_marginal = p_joint.sum(axis=0) p_state_marginal = p_joint.sum(axis=1) p_init = p_state_marginal # Make the Y|X distribution p_state_given_obs = (p_joint / p_obs_marginal).T # Let's make a test observation sequence seq = np.asarray([1, 1, 0, 1, 1, 1, 0, 0]) # Then our conditional probability table can be constructed directly as prob_d = np.asarray([p_state_given_obs[i] for i in seq]).T # Make a three-channel stack prob_mc = np.stack([prob_d, 1 - prob_d, prob_d[:, ::-1]]) path, logp = librosa.sequence.viterbi_discriminative( prob_mc, transition, p_state=p_state_marginal, p_init=p_init, return_logp=True ) # Check the second code path path2 = librosa.sequence.viterbi_discriminative( prob_mc, transition, p_state=p_state_marginal, p_init=p_init, return_logp=False ) assert np.array_equal(path, path2) path0, logp0 = librosa.sequence.viterbi_discriminative( prob_mc[0], transition, p_state=p_state_marginal, p_init=p_init, return_logp=True, ) assert np.allclose(path0, path[0]) assert np.allclose(logp0, logp[0]) path1, logp1 = librosa.sequence.viterbi_discriminative( prob_mc[1], transition, p_state=p_state_marginal, p_init=p_init, return_logp=True, ) assert np.allclose(path1, path[1]) assert np.allclose(logp1, logp[1]) path2, logp2 = librosa.sequence.viterbi_discriminative( prob_mc[2], transition, p_state=p_state_marginal, p_init=p_init, return_logp=True, ) assert np.allclose(path2, path[2]) assert np.allclose(logp2, logp[2]) def test_viterbi_discriminative_example_init(): # A pre-baked example with coin tosses transition = np.asarray([[0.75, 0.25], [0.25, 0.75]]) # Joint XY model p_joint = np.asarray([[0.25, 0.25], [0.1, 0.4]]) # marginals p_obs_marginal = p_joint.sum(axis=0) p_state_marginal = p_joint.sum(axis=1) p_init = np.asarray([0.5, 0.5]) # Make the Y|X distribution p_state_given_obs = (p_joint / p_obs_marginal).T # Let's make a test observation sequence seq = np.asarray([1, 1, 0, 1, 1, 1, 0, 0]) # Then our conditional probability table can be constructed directly as prob_d = np.asarray([p_state_given_obs[i] for i in seq]).T path, logp = librosa.sequence.viterbi_discriminative( prob_d, transition, p_state=p_state_marginal, p_init=p_init, return_logp=True ) path2, logp2 = librosa.sequence.viterbi_discriminative( prob_d, transition, p_state=p_state_marginal, return_logp=True ) assert np.array_equal(path, path2) assert np.allclose(logp, logp2) @pytest.fixture(scope="module") def x_disc(rng_mod): x = rng_mod.random(size=(3, 5)) ** 2 x /= x.sum(axis=0, keepdims=True) return x @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize( "trans", [ np.ones((3, 3), dtype=float), np.ones((3, 2), dtype=float) * 0.5, np.ones((2, 2), dtype=float) * 0.5, np.asarray([[1, 1, -1], [1, 1, -1], [1, 1, -1]], dtype=float), ], ids=["sum>1", "bad shape", "too small", "negative"], ) def test_viterbi_discriminative_bad_transition(x_disc, trans): librosa.sequence.viterbi_discriminative(x_disc, trans) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("trans", [np.ones((3, 3), dtype=float) / 3]) @pytest.mark.parametrize( "p_init", [ np.ones(3, dtype=float), np.ones(4, dtype=float) / 4.0, np.asarray([1, 1, -1], dtype=float), ], ids=["sum>1", "too many states", "negative"], ) def test_viterbi_discriminative_bad_init(p_init, trans, x_disc): librosa.sequence.viterbi_discriminative(x_disc, trans, p_init=p_init) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("trans", [np.ones((3, 3), dtype=float) / 3]) @pytest.mark.parametrize( "p_state", [ np.ones(3, dtype=float), np.ones(4, dtype=float) / 4.0, np.asarray([1, 1, -1], dtype=float), ], ids=["sum>1", "too many states", "negative"], ) def test_viterbi_discriminative_bad_marginal(x_disc, trans, p_state): librosa.sequence.viterbi_discriminative(x_disc, trans, p_state=p_state) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("trans", [np.ones((3, 3), dtype=float) / 3]) @pytest.mark.parametrize( "x", [ np.zeros((3, 5), dtype=float), np.ones((3, 5), dtype=float), np.asarray([[1, 1, -1], [0, 0, 1], [0, 0, 0]], dtype=float), ], ids=["zeros", "ones", "neg"], ) def test_viterbi_discriminative_bad_obs(x, trans): librosa.sequence.viterbi_discriminative(x, trans) # Multi-label viterbi def test_viterbi_binary_example(): # 0 stays 0, # 1 is uninformative transition = np.asarray([[0.9, 0.1], [0.5, 0.5]]) # Initial state distribution p_init = np.asarray([0.25, 0.75]) p_binary = np.asarray([0.25, 0.5, 0.75, 0.1, 0.1, 0.8, 0.9]) p_full = np.vstack((1 - p_binary, p_binary)) # Compute the viterbi_binary result for one class path, logp = librosa.sequence.viterbi_binary( p_binary, transition, p_state=p_init[1:], p_init=p_init[1:], return_logp=True ) # And the full multi-label result path_c, logp_c = librosa.sequence.viterbi_binary( p_full, transition, p_state=p_init, p_init=p_init, return_logp=True ) path_c2 = librosa.sequence.viterbi_binary( p_full, transition, p_state=p_init, p_init=p_init, return_logp=False ) # Check that the single and multilabel cases agree assert np.allclose(logp, logp_c[1]) assert np.array_equal(path[0], path_c[1]) assert np.array_equal(path_c, path_c2) # And do an explicit multi-class comparison path_d, logp_d = librosa.sequence.viterbi_discriminative( p_full, transition, p_state=p_init, p_init=p_init, return_logp=True ) assert np.allclose(logp[0], logp_d) assert np.array_equal(path[0], path_d) def test_viterbi_binary_multi(): # 0 stays 0, # 1 is uninformative transition = np.asarray([[0.9, 0.1], [0.5, 0.5]]) # Initial state distribution p_init = np.asarray([0.25, 0.75]) p_binary = np.asarray([[0.25, 0.5, 0.75, 0.1, 0.1, 0.8, 0.9]]) # Make a three-channel stack p_mc = np.stack([p_binary, 1 - p_binary, p_binary[::-1]]) path, logp = librosa.sequence.viterbi_binary( p_mc, transition, p_state=p_init[1:], p_init=p_init[1:], return_logp=True ) path2 = librosa.sequence.viterbi_binary( p_mc, transition, p_state=p_init[1:], p_init=p_init[1:], ) # Verify that both branches agree on path assert np.array_equal(path, path2) # Check each channel independently for i in range(len(p_mc)): pi, logpi = librosa.sequence.viterbi_binary( p_mc[i], transition, p_state=p_init[1:], p_init=p_init[1:], return_logp=True ) assert np.array_equal(path[i], pi) assert np.array_equal(path[i].shape, pi.shape) assert np.allclose(logpi, logp[i]) def test_viterbi_binary_example_init(): # 0 stays 0, # 1 is uninformative transition = np.asarray([[0.9, 0.1], [0.5, 0.5]]) # Initial state distribution p_init = np.asarray([0.5, 0.5]) p_binary = np.asarray([0.25, 0.5, 0.75, 0.1, 0.1, 0.8, 0.9]) p_full = np.vstack((1 - p_binary, p_binary)) # And the full multi-label result path_c, logp_c = librosa.sequence.viterbi_binary( p_full, transition, p_state=p_init, p_init=p_init, return_logp=True ) path_c2, logp_c2 = librosa.sequence.viterbi_binary( p_full, transition, p_state=p_init, return_logp=True ) # Check that the single and multilabel cases agree assert np.allclose(logp_c, logp_c2) assert np.array_equal(path_c, path_c2) @pytest.fixture(scope="module") def x_data(rng_mod): return rng_mod.random(size=(3, 5)) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize( "trans", [ np.ones((2, 2), dtype=float), np.ones((3, 3), dtype=float) / 3, np.ones((3, 5, 5), dtype=float), np.asarray([[2, -1], [2, -1]]), ], ids=["sum>1", "wrong size", "wrong shape", "negative"], ) def test_viterbi_binary_bad_transition(x_data, trans): librosa.sequence.viterbi_binary(x_data, trans) @pytest.mark.parametrize("trans", [np.ones((2, 2), dtype=float) * 0.5]) @pytest.mark.parametrize( "p_init", [2 * np.ones(3, dtype=float), np.ones(4, dtype=float), -np.ones(3, dtype=float)], ids=["too big", "wrong shape", "negative"], ) @pytest.mark.xfail(raises=librosa.ParameterError) def test_viterbi_binary_bad_init(x_data, trans, p_init): librosa.sequence.viterbi_binary(x_data, trans, p_init=p_init) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("trans", [np.ones((2, 2), dtype=float) * 0.5]) @pytest.mark.parametrize( "p_state", [2 * np.ones(3, dtype=float), np.ones(4, dtype=float), -np.ones(3, dtype=float)], ids=["too big", "bad shape", "negative"], ) def test_viterbi_binary_bad_marginal(p_state, trans, x_data): librosa.sequence.viterbi_binary(x_data, trans, p_state=p_state) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("trans", [np.ones((2, 2), dtype=float) * 0.5]) @pytest.mark.parametrize( "x", [-np.ones((3, 5), dtype=float), 2 * np.ones((3, 5), dtype=float)], ids=["non-positive", "too big"], ) def test_viterbi_binary_bad_obs(x, trans): librosa.sequence.viterbi_binary(x, trans) # Transition operator constructors @pytest.mark.parametrize("n", range(1, 4)) def test_trans_uniform(n): A = librosa.sequence.transition_uniform(n) assert A.shape == (n, n) assert np.allclose(A, 1.0 / n) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("n", [0, None]) def test_trans_uniform_badshape(n): librosa.sequence.transition_uniform(n) @pytest.mark.parametrize("n,p", [(2, 0.5), (3, 0.5), (3, [0.8, 0.7, 0.5])]) def test_trans_loop(n, p): A = librosa.sequence.transition_loop(n, p) # Right shape assert A.shape == (n, n) # diag is correct assert np.allclose(np.diag(A), p) # we have well-formed distributions assert np.all(A >= 0) assert np.allclose(A.sum(axis=1), 1) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize( "n,p", [(1, 0.5), (None, 0.5), (3, 1.5), (3, -0.25), (3, [0.5, 0.2])], ids=[ "missing states", "wrong states", "not probability", "neg prob", "shape mismatch", ], ) def test_trans_loop_fail(n, p): librosa.sequence.transition_loop(n, p) @pytest.mark.parametrize("n,p", [(2, 0.5), (3, 0.5), (3, [0.8, 0.7, 0.5])]) def test_trans_cycle(n, p): A = librosa.sequence.transition_cycle(n, p) # Right shape assert A.shape == (n, n) # diag is correct assert np.allclose(np.diag(A), p) for i in range(n): assert A[i, np.mod(i + 1, n)] == 1 - A[i, i] # we have well-formed distributions assert np.all(A >= 0) assert np.allclose(A.sum(axis=1), 1) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize( "n,p", [(1, 0.5), (None, 0.5), (3, 1.5), (3, -0.25), (3, [0.5, 0.2])], ids=["too few states", "wrong n_states", "p>1", "p<0", "shape mismatch"], ) def test_trans_cycle_fail(n, p): librosa.sequence.transition_cycle(n, p) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("n", [1.5, 0]) def test_trans_local_nstates_fail(n): librosa.sequence.transition_local(n, 3) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("width", [-1, 0, [2, 3]]) def test_trans_local_width_fail(width): librosa.sequence.transition_local(5, width) def test_trans_local_wrap_const(): A = librosa.sequence.transition_local(5, 3, window="triangle", wrap=True) A_true = np.asarray( [ [0.5, 0.25, 0.0, 0.0, 0.25], [0.25, 0.5, 0.25, 0.0, 0.0], [0.0, 0.25, 0.5, 0.25, 0.0], [0.0, 0.0, 0.25, 0.5, 0.25], [0.25, 0.0, 0.0, 0.25, 0.5], ] ) assert np.allclose(A, A_true) def test_trans_local_nowrap_const(): A = librosa.sequence.transition_local(5, 3, window="triangle", wrap=False) A_true = np.asarray( [ [2.0 / 3, 1.0 / 3, 0.0, 0.0, 0.0], [0.25, 0.5, 0.25, 0.0, 0.0], [0.0, 0.25, 0.5, 0.25, 0.0], [0.0, 0.0, 0.25, 0.5, 0.25], [0.0, 0.0, 0.0, 1.0 / 3, 2.0 / 3], ] ) assert np.allclose(A, A_true) def test_trans_local_wrap_var(): A = librosa.sequence.transition_local(5, [2, 1, 3, 3, 2], window="ones", wrap=True) A_true = np.asarray( [ [0.5, 0.0, 0.0, 0.0, 0.5], [0.0, 1.0, 0.0, 0.0, 0.0], [0.0, 1.0 / 3, 1.0 / 3, 1.0 / 3, 0.0], [0.0, 0.0, 1.0 / 3, 1.0 / 3, 1.0 / 3], [0.0, 0.0, 0.0, 0.5, 0.5], ] ) assert np.allclose(A, A_true) def test_trans_local_nowrap_var(): A = librosa.sequence.transition_local(5, [2, 1, 3, 3, 2], window="ones", wrap=False) A_true = np.asarray( [ [1.0, 0.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0, 0.0], [0.0, 1.0 / 3, 1.0 / 3, 1.0 / 3, 0.0], [0.0, 0.0, 1.0 / 3, 1.0 / 3, 1.0 / 3], [0.0, 0.0, 0.0, 0.5, 0.5], ] ) assert np.allclose(A, A_true) @pytest.mark.parametrize("gap_onset", [1, np.inf]) @pytest.mark.parametrize("gap_extend", [1, np.inf]) @pytest.mark.parametrize("knight", [False, True]) @pytest.mark.parametrize("backtrack", [False, True]) def test_rqa_edge(gap_onset, gap_extend, knight, backtrack: bool): rec = np.asarray([[0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 1, 1], [0, 0, 0, 1]]) kwargs = dict(gap_onset=gap_onset, gap_extend=gap_extend, knight_moves=knight) if backtrack: out = librosa.sequence.rqa(rec, backtrack=backtrack, **kwargs) score, path = out __validate_rqa_results( rec, score, path, gap_onset, gap_extend, backtrack, knight ) assert len(path) == 3 else: out = librosa.sequence.rqa(rec, backtrack=backtrack, **kwargs) # without backtracking, make sure the output is just the score matrix assert out.shape == rec.shape @pytest.mark.parametrize("gap_onset", [1, np.inf]) @pytest.mark.parametrize("gap_extend", [1, np.inf]) @pytest.mark.parametrize("knight", [False, True]) def test_rqa_empty(gap_onset, gap_extend, knight): rec = np.zeros((5, 5)) score, path = librosa.sequence.rqa( rec, gap_onset=gap_onset, gap_extend=gap_extend, knight_moves=knight, backtrack=True, ) assert score.shape == rec.shape assert np.allclose(score, 0) assert path.shape == (0, 2) @pytest.mark.parametrize("shape", [(1, 8), (8, 1)]) @pytest.mark.parametrize("backtrack", [False, True]) def test_rqa_single_axis(shape, backtrack: bool): rec = np.arange(np.prod(shape), dtype=float).reshape(shape) if backtrack: score, path = librosa.sequence.rqa(rec, backtrack=backtrack) assert score.shape == rec.shape assert path.shape == (1, 2) assert tuple(path[0]) == np.unravel_index(np.argmax(rec), rec.shape) else: score = librosa.sequence.rqa(rec, backtrack=backtrack) assert score.shape == rec.shape @pytest.mark.parametrize("gap_onset", [1, np.inf]) @pytest.mark.parametrize("gap_extend", [1, np.inf]) @pytest.mark.parametrize("knight", [False, True]) @pytest.mark.parametrize("backtrack", [False, True]) def test_rqa_interior(gap_onset, gap_extend, knight, backtrack: bool): rec = np.asarray([[0, 0, 0, 1], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 0]]) kwargs = dict(gap_onset=gap_onset, gap_extend=gap_extend, knight_moves=knight) if backtrack: out = librosa.sequence.rqa(rec, backtrack=backtrack, **kwargs) score, path = out __validate_rqa_results( rec, score, path, gap_onset, gap_extend, backtrack, knight ) assert len(path) == 2 else: out = librosa.sequence.rqa(rec, backtrack=backtrack, **kwargs) # without backtracking, make sure the output is just the score matrix assert out.shape == rec.shape @pytest.mark.parametrize("gap_onset", [1, np.inf]) @pytest.mark.parametrize("gap_extend", [1, np.inf]) def test_rqa_gaps(gap_onset, gap_extend): rec = np.ones((5, 5)) librosa.sequence.rqa(rec, gap_onset=gap_onset, gap_extend=gap_extend) @pytest.mark.xfail(raises=librosa.ParameterError) def test_rqa_bad_onset(): rec = np.ones((5, 5)) librosa.sequence.rqa(rec, gap_onset=-1) @pytest.mark.xfail(raises=librosa.ParameterError) def test_rqa_bad_extend(): rec = np.ones((5, 5)) librosa.sequence.rqa(rec, gap_extend=-1) def __validate_rqa_results(rec, score, path, gap_onset, gap_extend, backtrack, knight): # Test maximal end-point assert np.all(score[tuple(path[-1])] >= score) # Test non-zero start point assert rec[tuple(path[0])] > 0 # If we can't have gaps, then all values must be nonzero if not np.isfinite(gap_onset) and not np.isfinite(gap_extend): assert np.all([rec[tuple(i)] > 0 for i in path]) path_diff = np.diff(path, axis=0) if knight: for d in path_diff: assert ( np.allclose(d, (1, 1)) or np.allclose(d, (1, 2)) or np.allclose(d, (2, 1)) ) else: # Without knight moves, only diagonal steps are allowed assert np.allclose(path_diff, 1) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1786462635.2332273 librosa-1.0.0/tests/test_util.py0000644000175100017510000014267315236640653016370 0ustar00runnerrunner#!/usr/bin/env python # CREATED:2014-01-18 14:09:05 by Brian McFee # unit tests for util routines from __future__ import annotations # Disable cache import os try: os.environ.pop("LIBROSA_CACHE_DIR") except KeyError: pass import platform import numpy as np import scipy.sparse import pytest import warnings import librosa from typing import Any, List, Union @pytest.mark.parametrize("frame_length", [4, 8]) @pytest.mark.parametrize("hop_length", [2, 4]) @pytest.mark.parametrize("y", [np.arange(32)]) @pytest.mark.parametrize("axis", [0, -1]) def test_frame1d(frame_length, hop_length, axis, y): y_frame = librosa.util.frame( y, frame_length=frame_length, hop_length=hop_length, axis=axis ) if axis == -1: y_frame = y_frame.T for i in range(y_frame.shape[0]): assert np.allclose( y_frame[i], y[i * hop_length : (i * hop_length + frame_length)] ) @pytest.mark.parametrize("frame_length", [4, 8]) @pytest.mark.parametrize("hop_length", [2, 4]) @pytest.mark.parametrize( "y, axis", [ (np.asfortranarray(np.vander(np.arange(16), 32)), -1), (np.ascontiguousarray(np.vander(np.arange(16), 32)), 0), ], ) def test_frame2d(frame_length, hop_length, axis, y): y_frame = librosa.util.frame( y, frame_length=frame_length, hop_length=hop_length, axis=axis ) if axis == -1: y_frame = y_frame.T y = y.T for i in range(y_frame.shape[0]): assert np.allclose( y_frame[i], y[i * hop_length : (i * hop_length + frame_length)] ) def test_frame_0stride(): x = np.arange(10) xpad = x[np.newaxis] xpad2 = np.atleast_2d(x) xf = librosa.util.frame(x, frame_length=3, hop_length=1) xfpad = librosa.util.frame(xpad, frame_length=3, hop_length=1) xfpad2 = librosa.util.frame(xpad2, frame_length=3, hop_length=1) assert np.allclose(xf, xfpad) assert np.allclose(xf, xfpad2) @pytest.mark.parametrize("frame_length", [5, 10]) @pytest.mark.parametrize("hop_length", [1, 2]) @pytest.mark.parametrize("ndim", [2, 3, 4, 5]) def test_frame_highdim(frame_length, hop_length, ndim, rng): x = rng.standard_normal(size=tuple([20] * ndim)) xf = librosa.util.frame(x, frame_length=frame_length, hop_length=hop_length) for i in range(x.shape[0]): xf0 = librosa.util.frame(x[i], frame_length=frame_length, hop_length=hop_length) assert np.allclose(xf[i], xf0) @pytest.mark.parametrize( "in_shape,axis,out_shape", [ ((20, 20, 20, 20), 0, (6, 10, 20, 20, 20)), ((20, 20, 20, 20), 1, (20, 6, 10, 20, 20)), ((20, 20, 20, 20), 2, (20, 20, 6, 10, 20)), ((20, 20, 20, 20), 3, (20, 20, 20, 6, 10)), ((20, 20, 20, 20), -1, (20, 20, 20, 10, 6)), ((20, 20, 20, 20), -2, (20, 20, 10, 6, 20)), ((20, 20, 20, 20), -3, (20, 10, 6, 20, 20)), ((20, 20, 20, 20), -4, (10, 6, 20, 20, 20)), ], ) def test_frame_targetaxis(in_shape, axis, out_shape): x = np.empty(in_shape) xf = librosa.util.frame(x, frame_length=10, hop_length=2, axis=axis) assert xf.shape == out_shape @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("axis", [0, -1]) @pytest.mark.parametrize("x", [np.arange(16)]) def test_frame_too_short(x, axis): librosa.util.frame(x, frame_length=17, hop_length=1, axis=axis) @pytest.mark.xfail(raises=librosa.ParameterError) def test_frame_bad_hop(): librosa.util.frame(np.arange(16), frame_length=4, hop_length=0) @pytest.mark.parametrize("y", [np.ones((16,)), np.ones((16, 16))]) @pytest.mark.parametrize("m", [0, 10]) @pytest.mark.parametrize("axis", [0, -1]) @pytest.mark.parametrize("mode", ["constant", "edge", "reflect"]) def test_pad_center(y, m, axis, mode): n = m + y.shape[axis] y_out = librosa.util.pad_center(y, size=n, axis=axis, mode=mode) n_len = y.shape[axis] n_pad = int((n - n_len) / 2) eq_slice = [slice(None)] * y.ndim eq_slice[axis] = slice(n_pad, n_pad + n_len) assert np.allclose(y, y_out[tuple(eq_slice)]) @pytest.mark.parametrize("y", [np.ones((16,)), np.ones((16, 16))]) @pytest.mark.parametrize("n", [0, 10]) @pytest.mark.parametrize("axis", [0, -1]) @pytest.mark.parametrize("mode", ["constant", "edge", "reflect"]) @pytest.mark.xfail(raises=librosa.ParameterError) def test_pad_center_fail(y, n, axis, mode): librosa.util.pad_center(y, size=n, axis=axis, mode=mode) @pytest.mark.parametrize("y", [np.ones((16,)), np.ones((16, 16))]) @pytest.mark.parametrize("m", [-5, 0, 5]) @pytest.mark.parametrize("axis", [0, -1]) def test_fix_length(y, m, axis): n = m + y.shape[axis] y_out = librosa.util.fix_length(y, size=n, axis=axis) eq_slice = [slice(None)] * y.ndim eq_slice[axis] = slice(y.shape[axis]) if n > y.shape[axis]: assert np.allclose(y, y_out[tuple(eq_slice)]) else: assert np.allclose(y[tuple(eq_slice)], y) @pytest.mark.parametrize("frames", [np.arange(20, 100, step=15)]) @pytest.mark.parametrize("x_min", [0, 20]) @pytest.mark.parametrize("x_max", [20, 70, 120]) @pytest.mark.parametrize("pad", [False, True]) def test_fix_frames(frames, x_min, x_max, pad): f_fix = librosa.util.fix_frames(frames, x_min=x_min, x_max=x_max, pad=pad) if x_min is not None: if pad: assert f_fix[0] == x_min assert np.all(f_fix >= x_min) if x_max is not None: if pad: assert f_fix[-1] == x_max assert np.all(f_fix <= x_max) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("frames", [np.arange(-20, 100)]) @pytest.mark.parametrize("x_min", [None, 0, 20]) @pytest.mark.parametrize("x_max", [None, 0, 20]) @pytest.mark.parametrize("pad", [False, True]) def test_fix_frames_fail_negative(frames, x_min, x_max, pad): librosa.util.fix_frames(frames, x_min=x_min, x_max=x_max, pad=pad) @pytest.mark.parametrize("norm", [np.inf, -np.inf, 0, 0.5, 1.0, 2.0, None]) @pytest.mark.parametrize( "ndims,axis", [(1, 0), (1, -1), (2, 0), (2, 1), (2, -1), (3, 0), (3, 1), (3, 2), (3, -1)], ) def test_normalize(ndims, norm, axis, rng): X = rng.standard_normal(size=tuple([4] * ndims)) X_norm = librosa.util.normalize(X, norm=norm, axis=axis) # Shape and dtype checks assert X_norm.dtype == X.dtype assert X_norm.shape == X.shape if norm is None: assert np.allclose(X, X_norm) return X_norm = np.abs(X_norm) if norm == np.inf: values = np.max(X_norm, axis=axis) elif norm == -np.inf: values = np.min(X_norm, axis=axis) elif norm == 0: # XXX: normalization here isn't quite right values = np.ones(1) else: values = np.sum(X_norm**norm, axis=axis) ** (1.0 / norm) assert np.allclose(values, np.ones_like(values)) @pytest.mark.parametrize("norm", ["inf", -0.5, -2]) @pytest.mark.parametrize("X", [np.ones((3, 3))]) @pytest.mark.xfail(raises=librosa.ParameterError) def test_normalize_badnorm(X, norm): librosa.util.normalize(X, norm=norm) @pytest.mark.parametrize("badval", [np.nan, np.inf, -np.inf]) @pytest.mark.xfail(raises=librosa.ParameterError) def test_normalize_bad_input(badval): X = np.ones((3, 3)) X[0] = badval librosa.util.normalize(X, norm=np.inf, axis=0) @pytest.mark.parametrize("fill", [7, "foo"]) @pytest.mark.parametrize("X", [np.ones((2, 2))]) @pytest.mark.xfail(raises=librosa.ParameterError) def test_normalize_badfill(X, fill): librosa.util.normalize(X, fill=fill) @pytest.mark.parametrize("x", [np.asarray([[0, 1, 2, 3]])]) @pytest.mark.parametrize( "threshold, result", [ (None, [[0, 1, 1, 1]]), (1, [[0, 1, 1, 1]]), (2, [[0, 1, 1, 1]]), (3, [[0, 1, 2, 1]]), (4, [[0, 1, 2, 3]]), ], ) def test_normalize_threshold(x, threshold, result): assert np.allclose(librosa.util.normalize(x, threshold=threshold), result) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("x", [np.asarray([[0, 1, 2, 3]])]) @pytest.mark.parametrize("threshold", [0, -1]) def test_normalize_threshold_fail(x, threshold): librosa.util.normalize(x, threshold=threshold) @pytest.mark.xfail(raises=librosa.ParameterError) def test_normalize_fill_l0(): X = np.ones((2, 2)) librosa.util.normalize(X, fill=True, norm=0) @pytest.mark.parametrize("norm", [1, 2, np.inf]) @pytest.mark.parametrize("X", [np.zeros((3, 3))]) def test_normalize_fill_allaxes(X, norm): Xn = librosa.util.normalize(X, fill=True, axis=None, norm=norm) if norm is np.inf: assert np.allclose(Xn, 1) else: assert np.allclose(np.sum(Xn**norm) ** (1.0 / norm), 1) @pytest.mark.parametrize("norm", [1, 2, np.inf]) @pytest.mark.parametrize("X", [np.zeros((3, 3))]) def test_normalize_nofill(X, norm): Xn = librosa.util.normalize(X, fill=False, norm=norm) assert np.allclose(Xn, 0) @pytest.mark.parametrize("X", [np.asarray([[0.0, 1], [0, 1]])]) @pytest.mark.parametrize("norm,value", [(1, 0.5), (2, np.sqrt(2) / 2), (np.inf, 1)]) @pytest.mark.parametrize("threshold", [0.5, 2]) def test_normalize_fill(X, threshold, norm, value): Xn = librosa.util.normalize(X, fill=True, norm=norm, threshold=threshold) assert np.allclose(Xn, value) @pytest.mark.parametrize("ndim", [1, 3]) @pytest.mark.parametrize("axis", [0, 1, -1]) @pytest.mark.parametrize("index", [False, True]) @pytest.mark.parametrize("value", [None, np.min, np.mean, np.max]) @pytest.mark.xfail(raises=librosa.ParameterError) def test_axis_sort_badndim(ndim, axis, index, value): data = np.zeros([2] * ndim) librosa.util.axis_sort(data, axis=axis, index=index, value=value) @pytest.mark.parametrize("ndim", [2]) @pytest.mark.parametrize("axis", [0, 1, -1]) @pytest.mark.parametrize("index", [False, True]) @pytest.mark.parametrize("value", [None, np.min, np.mean, np.max]) def test_axis_sort(ndim, axis, index, value, rng): data = rng.standard_normal(size=tuple([10] * ndim)) if index: Xsorted, idx = librosa.util.axis_sort(data, axis=axis, index=index, value=value) cmp_slice = [slice(None)] * ndim cmp_slice[axis] = idx assert np.allclose(data[tuple(cmp_slice)], Xsorted) else: Xsorted = librosa.util.axis_sort(data, axis=axis, index=index, value=value) compare_axis = np.mod(1 - axis, 2) if value is None: value = np.argmax sort_values = value(Xsorted, axis=compare_axis) assert np.allclose(sort_values, np.sort(sort_values)) @pytest.mark.parametrize( "int_from, int_to", [ (np.asarray([[0, 2], [0, 4], [3, 6]]), np.zeros((0, 2), dtype=int)), (np.zeros((0, 2), dtype=int), np.asarray([[0, 2], [0, 4], [3, 6]])), ], ) @pytest.mark.xfail(raises=librosa.ParameterError) def test_match_intervals_empty(int_from, int_to): librosa.util.match_intervals(int_from, int_to) @pytest.mark.xfail(raises=librosa.ParameterError) def test_match_intervals_strict_fail(): int_from = np.asarray([[0, 3], [2, 4], [5, 7]]) int_to = np.asarray([[0, 2], [0, 4]]) librosa.util.match_intervals(int_from, int_to, strict=True) @pytest.mark.parametrize("int_from", [np.asarray([[0, 3], [2, 4], [5, 7]])]) @pytest.mark.parametrize("int_to", [np.asarray([[0, 2], [0, 4], [3, 6]])]) @pytest.mark.parametrize("matches", [np.asarray([1, 1, 2])]) def test_match_intervals_strict(int_from, int_to, matches): test_matches = librosa.util.match_intervals(int_from, int_to, strict=True) assert np.array_equal(matches, test_matches) @pytest.mark.parametrize("int_from", [np.asarray([[0, 3], [2, 4], [5, 7]])]) @pytest.mark.parametrize( "int_to,matches", [ (np.asarray([[0, 2], [0, 4], [3, 6]]), np.asarray([1, 1, 2])), (np.asarray([[0, 2], [0, 4]]), np.asarray([1, 1, 1])), ], ) def test_match_intervals_nonstrict(int_from, int_to, matches): test_matches = librosa.util.match_intervals(int_from, int_to, strict=False) assert np.array_equal(matches, test_matches) @pytest.mark.parametrize("n", [1, 5, 20, 100]) @pytest.mark.parametrize("m", [1, 5, 20, 100]) def test_match_events(n, m, rng): ev1 = np.abs(rng.standard_normal(size=n)) ev2 = np.abs(rng.standard_normal(size=m)) match = librosa.util.match_events(ev1, ev2) for i in range(len(match)): values = np.asarray([np.abs(ev1[i] - e2) for e2 in ev2]) assert not np.any(values < values[match[i]]) @pytest.mark.parametrize( "ev1,ev2", [(np.array([]), np.arange(5)), (np.arange(5), np.array([]))] ) @pytest.mark.xfail(raises=librosa.ParameterError) def test_match_events_failempty(ev1, ev2): librosa.util.match_events(ev1, ev2) @pytest.mark.parametrize("events_from", [np.asarray([5, 15, 25])]) @pytest.mark.parametrize("events_to", [np.asarray([0, 10, 20, 30])]) @pytest.mark.parametrize( "left,right,target", [(False, True, [10, 20, 30]), (True, False, [0, 10, 20])] ) def test_match_events_onesided(events_from, events_to, left, right, target): events_from = np.asarray(events_from) events_to = np.asarray(events_to) match = librosa.util.match_events(events_from, events_to, left=left, right=right) assert np.allclose(target, events_to[match]) def test_match_events_twosided(): events_from = np.asarray([5, 15, 25]) events_to = np.asarray([5, 15, 25, 30]) match = librosa.util.match_events(events_from, events_to, left=False, right=False) assert np.allclose(match, [0, 1, 2]) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize( "events_from,events_to,left,right", [ ([40, 15, 25], [0, 10, 20, 30], False, True), # right-sided fail ([-1, 15, 25], [0, 10, 20, 30], True, False), # left-sided fail ([-1, 15, 25], [0, 10, 20, 30], False, False), # two-sided fail ], ) def test_match_events_onesided_fail(events_from, events_to, left, right): events_from = np.asarray(events_from) events_to = np.asarray(events_to) librosa.util.match_events(events_from, events_to, left=left, right=right) @pytest.mark.parametrize("ndim, axis", [(n, m) for n in range(1, 5) for m in range(n)]) def test_localmax(ndim, axis, rng): data = rng.standard_normal(size=tuple([7] * ndim)) lm = librosa.util.localmax(data, axis=axis) for hits in np.argwhere(lm): for offset in [-1, 1]: compare_idx = hits.copy() compare_idx[axis] += offset if compare_idx[axis] < 0: continue if compare_idx[axis] >= data.shape[axis]: continue if offset < 0: assert data[tuple(hits)] > data[tuple(compare_idx)] else: assert data[tuple(hits)] >= data[tuple(compare_idx)] @pytest.mark.parametrize("ndim, axis", [(n, m) for n in range(1, 5) for m in range(n)]) def test_localmin(ndim, axis, rng): data = rng.standard_normal(size=tuple([7] * ndim)) lm = librosa.util.localmin(data, axis=axis) for hits in np.argwhere(lm): for offset in [-1, 1]: compare_idx = hits.copy() compare_idx[axis] += offset if compare_idx[axis] < 0: continue if compare_idx[axis] >= data.shape[axis]: continue if offset < 0: assert data[tuple(hits)] < data[tuple(compare_idx)] else: assert data[tuple(hits)] <= data[tuple(compare_idx)] @pytest.mark.parametrize("d", [1, 5, 10, 100]) @pytest.mark.parametrize("pre_max", [0, 1, 10]) @pytest.mark.parametrize("post_max", [1, 10]) @pytest.mark.parametrize("pre_avg", [0, 1, 10]) @pytest.mark.parametrize("post_avg", [1, 10]) @pytest.mark.parametrize("wait", [0, 1, 10]) @pytest.mark.parametrize("delta", [0.05, 100.0]) @pytest.mark.parametrize("method", ["greedy", "dp_count", "dp_value"]) def test_peak_pick(d, pre_max, post_max, pre_avg, post_avg, delta, wait, method, rng): x = rng.standard_normal(size=d) ** 2 peaks = librosa.util.peak_pick( x, pre_max=pre_max, post_max=post_max, pre_avg=pre_avg, post_avg=post_avg, delta=delta, wait=wait, sparse=True, method=method, ) dpeaks = librosa.util.peak_pick( x, pre_max=pre_max, post_max=post_max, pre_avg=pre_avg, post_avg=post_avg, delta=delta, wait=wait, sparse=False, method=method, ) for i in peaks: # Test 1: is it a peak in this window? s = i - pre_max if s < 0: s = 0 t = i + post_max diff = x[i] - np.max(x[s:t]) assert diff > 0 or np.isclose(diff, 0, rtol=1e-3, atol=1e-4) # Test 2: is it a big enough peak to count? s = i - pre_avg if s < 0: s = 0 t = i + post_avg diff = x[i] - (delta + np.mean(x[s:t])) assert diff > 0 or np.isclose(diff, 0, rtol=1e-3, atol=1e-4) # Test 3: peak separation assert not np.any(np.diff(peaks) <= wait) # Test 4: check dense vs sparse assert np.allclose(np.flatnonzero(dpeaks), peaks) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize( "pre_max,post_max,pre_avg,post_avg,delta,wait", [ (-1, 1, 1, 1, 0.05, 1), # negative pre-max (1, -1, 1, 1, 0.05, 1), # negative post-max (1, 0, 1, 1, 0.05, 1), # 0 post-max (1, 1, -1, 1, 0.05, 1), # negative pre-avg (1, 1, 1, -1, 0.05, 1), # negative post-avg (1, 1, 1, 0, 0.05, 1), # zero post-avg (1, 1, 1, 1, -0.05, 1), # negative delta (1, 1, 1, 1, 0.05, -1), # negative wait ], ) @pytest.mark.parametrize("d", [1, 5, 10, 100]) def test_peak_pick_fail(d, pre_max, post_max, pre_avg, post_avg, delta, wait, rng): x = rng.standard_normal(size=d) ** 2 librosa.util.peak_pick( x, pre_max=pre_max, post_max=post_max, pre_avg=pre_avg, post_avg=post_avg, delta=delta, wait=wait, ) @pytest.mark.xfail(raises=librosa.ParameterError) def test_peak_pick_badmethod(): # suppress mypy type check here: librosa.util.peak_pick( np.zeros(100), pre_max=3, post_max=3, pre_avg=3, post_avg=3, delta=1, wait=1, method="foo", # type: ignore ) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("ndim", [3, 4]) def test_sparsify_rows_ndimfail(ndim): X = np.zeros([2] * ndim) librosa.util.sparsify_rows(X) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("quantile", [1.0, -1, 2.0]) @pytest.mark.parametrize("X", [np.ones((3, 3))]) def test_sparsify_rows_badquantile(X, quantile): librosa.util.sparsify_rows(X, quantile=quantile) @pytest.mark.parametrize("dtype", [None, np.float32, np.float64]) @pytest.mark.parametrize("ref_dtype", [np.float32, np.float64]) def test_sparsify_rows_dtype(dtype, ref_dtype): x = np.ones(10, dtype=ref_dtype) xs = librosa.util.sparsify_rows(x, dtype=dtype) if dtype is None: assert xs.dtype == x.dtype else: assert xs.dtype == dtype @pytest.mark.parametrize("ndim", [1, 2]) @pytest.mark.parametrize("d", [1, 5, 10, 100]) @pytest.mark.parametrize("q", [0.0, 0.01, 0.25, 0.5, 0.99]) def test_sparsify_rows(ndim, d, q, rng): X = rng.standard_normal(size=tuple([d] * ndim)) ** 4 # always ndarray as size is specified xs = librosa.util.sparsify_rows(X, quantile=q) assert isinstance(xs, scipy.sparse.sparray) assert not isinstance(xs, scipy.sparse.spmatrix) if ndim == 1: X = X.reshape((1, -1)) assert np.allclose(xs.shape, X.shape) # And make sure that xs matches X on nonzeros xsd = xs.toarray() # always ndarray now for i in range(xs.shape[0]): # Get column indices for row i using CSR internal structure # (1D sparse slicing is not supported for sparse arrays) row_start = xs.indptr[i] row_end = xs.indptr[i+1] row_indices = xs.indices[row_start:row_end] assert np.allclose(xsd[i, row_indices], X[i, row_indices]) # Compute row-wise magnitude marginals v_in = np.sum(np.abs(X), axis=-1) v_out = np.sum(np.abs(xsd), axis=-1) # Ensure that v_out retains 1-q fraction of v_in assert np.all(v_out >= (1.0 - q) * v_in) @pytest.fixture(scope="module") def search_files(tmp_path_factory): tmp_path = tmp_path_factory.mktemp("audio_test") test_files = [ "test1_22050.mp3", "test1_22050.wav", "test1_44100.wav", "test2_8000.wav", ] paths = [] subdir = tmp_path / "data" subdir.mkdir(parents=True, exist_ok=True) for filename in test_files: file_path = subdir / filename file_path.touch() # Creates empty file paths.append(str(file_path)) return tmp_path, paths @pytest.mark.parametrize("ext", [None, "wav", "WAV", ["wav"], ["WAV"]]) @pytest.mark.parametrize( "case_sensitive", list({False} | {platform.system() != "Windows"}) ) @pytest.mark.parametrize("limit", [None, 1, 2]) @pytest.mark.parametrize("offset", [0, 1, -1]) def test_find_files(search_files, ext, case_sensitive, limit, offset): searchdir, output = search_files files = librosa.util.find_files( searchdir, ext=ext, recurse=True, case_sensitive=case_sensitive, limit=limit, offset=offset, ) targets = output if ext is not None: # If we're only seeking wavs, bump off the mp3 file targets = targets[1:] s1 = slice(offset, None) s2 = slice(limit) if case_sensitive and ext not in (None, "wav", ["wav"]): assert len(files) == 0 else: assert set(files) == set(targets[s1][s2]) def test_find_files_nonrecurse(search_files): searchdir, _ = search_files files = librosa.util.find_files(searchdir, recurse=False) assert len(files) == 0 # fail if ext is not none, we're case-sensitive, and looking for WAV @pytest.mark.parametrize("ext", ["WAV", ["WAV"]]) def test_find_files_case_sensitive(search_files, ext): searchdir, files = search_files files = librosa.util.find_files(searchdir, ext=ext, case_sensitive=True) # On windows, this test won't work if platform.system() != "Windows": assert len(files) == 0 @pytest.mark.parametrize("x_in", np.linspace(-2, 2, num=6)) @pytest.mark.parametrize("cast", [None, np.floor, np.ceil]) def test_valid_int(x_in, cast): z = librosa.util.valid_int(x_in, cast=cast) assert isinstance(z, int) if cast is None: assert z == int(np.floor(x_in)) else: assert z == int(cast(x_in)) @pytest.mark.parametrize("x", np.linspace(-2, 2, num=3)) @pytest.mark.parametrize("cast", [7]) @pytest.mark.xfail(raises=librosa.ParameterError) def test_valid_int_fail(x, cast): # Test with a non-callable cast operator librosa.util.valid_int(x, cast=cast) @pytest.mark.parametrize("x", [1, 64]) def test_is_positive_int(x): assert librosa.util.is_positive_int(x) is True @pytest.mark.parametrize( "x", [None, 0, -1, 1.1, np.float64(1.2), -np.inf, np.finfo(float).eps] ) def test_is_positive_int_fail(x): assert librosa.util.is_positive_int(x) is False @pytest.mark.parametrize( "ivals", [ np.asarray([[0, 1], [1, 2]]), np.asarray([[0, 0], [1, 1]]), np.asarray([[0, 2], [1, 2]]), ], ) def test_valid_intervals(ivals): librosa.util.valid_intervals(ivals) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize( "ivals", [ np.asarray([]), np.arange(2), np.ones((2, 2, 2)), np.ones((2, 3)), ], # ndim=0 # ndim=1 # ndim=3 ) # ndim=2, shape[1] != 2 def test_valid_intervals_badshape(ivals): # fail if ndim != 2 or shape[1] != 2 librosa.util.valid_intervals(ivals) @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("intval", [np.asarray([[0, 1], [2, 1]])]) def test_valid_intervals_fail(intval): # Test for issue #712: intervals must have non-negative duration librosa.util.valid_intervals(intval) def test_warning_deprecated(): @librosa.util.decorators.deprecated( version="old_version", version_removed="new_version" ) def __placeholder(): return True with pytest.warns(FutureWarning, match="Deprecated"): x = __placeholder() # Make sure we still get the right value assert x is True def test_warning_moved(): @librosa.util.decorators.moved( moved_from="from", version="old_version", version_removed="new_version" ) def __placeholder(): return True with warnings.catch_warnings(record=True) as out: x = __placeholder() # Make sure we still get the right value assert x is True # And that the warning triggered assert len(out) > 0 # And that the category is correct assert out[0].category is FutureWarning # And that it says the right thing (roughly) assert "moved" in str(out[0].message).lower() def test_warning_future_default(): @librosa.util.decorators.future_default(param_name="foo", old_default=1, new_default=2, version="1.0") def __placeholder(foo=1): return foo**2 with warnings.catch_warnings(record=True) as out: v1 = __placeholder() v2 = __placeholder(foo=1) v3 = __placeholder(foo=2) assert v1 == 1 assert v2 == 1 assert v3 == 4 # And that the warning triggered for the default case assert len(out) > 0 # And that the category is correct assert out[0].category is FutureWarning # And that it says the right thing (roughly) assert "default" in str(out[0].message).lower() def test_warning_rename_kw_pass(): ov = librosa.util.Deprecated() nv = 23 with warnings.catch_warnings(record=True) as out: v = librosa.util.rename_kw( old_name="old", old_value=ov, new_name="new", new_value=nv, version_deprecated="0", version_removed="1", ) assert v == nv # Make sure no warning triggered assert len(out) == 0 def test_warning_rename_kw_fail(): ov = 27 nv = 23 with pytest.warns(FutureWarning, match="renamed"): v = librosa.util.rename_kw( old_name="old", old_value=ov, new_name="new", new_value=nv, version_deprecated="0", version_removed="1", ) assert v == ov @pytest.mark.parametrize("idx", [np.arange(10, 90, 10), np.arange(10, 90, 15)]) @pytest.mark.parametrize("idx_min", [None, 5, 15]) @pytest.mark.parametrize("idx_max", [None, 85, 100]) @pytest.mark.parametrize("step", [None, 2]) @pytest.mark.parametrize("pad", [False, True]) def test_index_to_slice(idx, idx_min, idx_max, step, pad): slices = librosa.util.index_to_slice( idx, idx_min=idx_min, idx_max=idx_max, step=step, pad=pad ) if pad: if idx_min is not None: assert slices[0].start == idx_min if idx.min() != idx_min: slices = slices[1:] if idx_max is not None: assert slices[-1].stop == idx_max if idx.max() != idx_max: slices = slices[:-1] if idx_min is not None: idx = idx[idx >= idx_min] if idx_max is not None: idx = idx[idx <= idx_max] idx = np.unique(idx) assert len(slices) == len(idx) - 1 for sl, start, stop in zip(slices, idx, idx[1:]): assert sl.start == start assert sl.stop == stop assert sl.step == step @pytest.mark.parametrize("aggregate", [None, np.mean, np.sum]) @pytest.mark.parametrize( "ndim,axis", [(1, 0), (1, -1), (2, 0), (2, 1), (2, -1), (3, 0), (3, 2), (3, -1)] ) def test_sync(aggregate, ndim, axis: int): data = np.ones([6] * ndim, dtype=float) # Make some slices that don't fill the entire dimension slices = [slice(1, 3), slice(3, 4)] dsync = librosa.util.sync(data, slices, aggregate=aggregate, axis=axis) # Check the axis shapes assert dsync.shape[axis] == len(slices) s_test = list(dsync.shape) del s_test[axis] s_orig = list(data.shape) del s_orig[axis] assert s_test == s_orig # The first slice will sum to 2 and have mean 1 idx: List[Union[slice, int]] = [slice(None)] * ndim idx[axis] = 0 if aggregate is np.sum: assert np.allclose(dsync[tuple(idx)], 2) else: assert np.allclose(dsync[tuple(idx)], 1) # The second slice will sum to 1 and have mean 1 idx[axis] = 1 assert np.allclose(dsync[tuple(idx)], 1) @pytest.mark.parametrize("aggregate", [np.mean, np.max]) def test_sync_slices(aggregate): x = np.arange(8, dtype=float) slices = [slice(0, 2), slice(2, 4), slice(4, 6), slice(6, 8)] xsync = librosa.util.sync(x, slices, aggregate=aggregate) if aggregate is np.mean: assert np.allclose(xsync, [0.5, 2.5, 4.5, 6.5]) elif aggregate is np.max: assert np.allclose(xsync, [1, 3, 5, 7]) else: assert False @pytest.mark.parametrize("aggregate", [np.mean, np.max]) @pytest.mark.parametrize("atype", [list, np.asarray]) def test_sync_frames(aggregate, atype): x = np.arange(8, dtype=float) frames = atype([0, 2, 4, 6, 8]) xsync = librosa.util.sync(x, frames, aggregate=aggregate) if aggregate is np.mean: assert np.allclose(xsync, [0.5, 2.5, 4.5, 6.5]) elif aggregate is np.max: assert np.allclose(xsync, [1, 3, 5, 7]) else: assert False @pytest.mark.parametrize("atype", [list, np.asarray]) @pytest.mark.parametrize("pad", [False, True]) def test_sync_frames_pad(atype, pad): x = np.arange(8, dtype=float) frames = atype([2, 4, 6]) xsync = librosa.util.sync(x, frames, pad=pad) if pad: assert np.allclose(xsync, [0.5, 2.5, 4.5, 6.5]) else: assert np.allclose(xsync, [2.5, 4.5]) @pytest.mark.parametrize("data", [np.mod(np.arange(135), 5)]) @pytest.mark.parametrize("idx", [["foo", "bar"], [None], [slice(None), None]]) @pytest.mark.xfail(raises=librosa.ParameterError) def test_sync_fail(data, idx): librosa.util.sync(data, idx) @pytest.mark.parametrize("power", [1, 2, 50, 100, np.inf]) @pytest.mark.parametrize("split_zeros", [False, True]) def test_softmask(power, split_zeros, rng): X = np.abs(rng.standard_normal(size=(10, 10))) X_ref = np.abs(rng.standard_normal(size=(10, 10))) # Zero out some rows X[3, :] = 0 X_ref[3, :] = 0 M = librosa.util.softmask(X, X_ref, power=power, split_zeros=split_zeros) assert np.all(0 <= M) and np.all(M <= 1) if split_zeros and np.isfinite(power): assert np.allclose(M[3, :], 0.5) else: assert not np.any(M[3, :]), M[3] def test_softmask_int(): X = 2 * np.ones((3, 3), dtype=np.int32) X_ref = np.vander(np.arange(3)) M1 = librosa.util.softmask(X, X_ref, power=1) M2 = librosa.util.softmask(X_ref, X, power=1) assert np.allclose(M1 + M2, 1) @pytest.mark.parametrize( "x,x_ref,power,split_zeros", [ (-np.ones(3), np.ones(3), 1, False), (np.ones(3), -np.ones(3), 1, False), (np.ones(3), np.ones(4), 1, False), (np.ones(3), np.ones(3), 0, False), (np.ones(3), np.ones(3), -1, False), ], ) @pytest.mark.xfail(raises=librosa.ParameterError) def test_softmask_fail(x, x_ref, power, split_zeros): librosa.util.softmask(x, x_ref, power=power, split_zeros=split_zeros) @pytest.mark.parametrize( "x,value", [ (1, np.finfo(np.float32).tiny), (np.ones(3, dtype=int), np.finfo(np.float32).tiny), (np.ones(3, dtype=np.float32), np.finfo(np.float32).tiny), (1.0, np.finfo(np.float64).tiny), (np.ones(3, dtype=np.float64), np.finfo(np.float64).tiny), (1j, np.finfo(np.complex128).tiny), (np.ones(3, dtype=np.complex64), np.finfo(np.complex64).tiny), (np.ones(3, dtype=np.complex128), np.finfo(np.complex128).tiny), ], ) def test_tiny(x, value): assert value == librosa.util.tiny(x) def test_util_fill_off_diagonal_8_8(): # Case 1: Square matrix (N=M) mut_x = np.ones((8, 8)) librosa.util.fill_off_diagonal(mut_x, radius=0.25) gt_x = np.array( [ [1, 1, 0, 0, 0, 0, 0, 0], [1, 1, 1, 0, 0, 0, 0, 0], [0, 1, 1, 1, 0, 0, 0, 0], [0, 0, 1, 1, 1, 0, 0, 0], [0, 0, 0, 1, 1, 1, 0, 0], [0, 0, 0, 0, 1, 1, 1, 0], [0, 0, 0, 0, 0, 1, 1, 1], [0, 0, 0, 0, 0, 0, 1, 1], ] ) assert np.array_equal(mut_x, gt_x) assert np.array_equal(mut_x, gt_x.T) def test_util_fill_off_diagonal_8_12(): # Case 2a: N!=M mut_x = np.ones((8, 12)) librosa.util.fill_off_diagonal(mut_x, radius=0.25) gt_x = np.array( [ [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0], [0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0], [0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0], [0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0], [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1], [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1], ] ) assert np.array_equal(mut_x, gt_x) # Case 2b: (N!=M).T mut_x = np.ones((8, 12)).T librosa.util.fill_off_diagonal(mut_x, radius=0.25) assert np.array_equal(mut_x, gt_x.T) @pytest.mark.parametrize("dtype_A", [np.float32, np.float64]) @pytest.mark.parametrize("dtype_B", [np.float32, np.float64]) def test_nnls_vector(dtype_A, dtype_B, rng): # Make a random basis A = rng.standard_normal(size=(7, 5)).astype(dtype_A) # Make a random latent vector x = rng.standard_normal(size=A.shape[1]) ** 2 B = A.dot(x).astype(dtype_B) x_rec = librosa.util.nnls(A, B) assert np.all(x_rec >= 0) assert np.sqrt(np.mean((B - A.dot(x_rec)) ** 2)) <= 1e-6 @pytest.mark.parametrize("dtype_A", [np.float32, np.float64]) @pytest.mark.parametrize("dtype_B", [np.float32, np.float64]) @pytest.mark.parametrize("x_size", [3, 10]) def test_nnls_matrix(dtype_A, dtype_B, x_size, rng): # Make a random basis A = rng.standard_normal(size=(5, 7)).astype(dtype_A) # Make a random latent matrix # when x_size is 3, B is 7x3 (smaller than A) x = rng.standard_normal(size=(A.shape[1], x_size)) ** 2 B = A.dot(x).astype(dtype_B) x_rec = librosa.util.nnls(A, B) assert np.all(x_rec >= 0) assert np.sqrt(np.mean((B - A.dot(x_rec)) ** 2)) <= 2e-4 @pytest.mark.parametrize("dtype_A", [np.float32, np.float64]) @pytest.mark.parametrize("dtype_B", [np.float32, np.float64]) @pytest.mark.parametrize("x_size", [16, 64, 128]) def test_nnls_multiblock(dtype_A, dtype_B, x_size, rng): # Make a random basis A = rng.standard_normal(size=(4, 192)).astype(dtype_A) # Make a random latent matrix # when x_size is 3, B is 7x3 (smaller than A) x = rng.standard_normal(size=(A.shape[1], x_size)) ** 2 B = A.dot(x).astype(dtype_B) x_rec = librosa.util.nnls(A, B) assert np.all(x_rec >= 0) assert np.sqrt(np.mean((B - A.dot(x_rec)) ** 2)) <= 2e-4 @pytest.fixture def psig(): # [[0, 1, 2, 3, 4]] # axis=1 or -1 ==> [-1.5, 1, 1, 1, -1.5] # axis=0 ==> [0, 0, 0, 0, 0] return np.arange(0, 5, dtype=float)[np.newaxis] @pytest.mark.parametrize("edge_order", [1, 2]) @pytest.mark.parametrize("axis", [0, 1, -1]) def test_cyclic_gradient(psig, edge_order, axis): grad = librosa.util.cyclic_gradient(psig, edge_order=edge_order, axis=axis) assert grad.shape == psig.shape assert grad.dtype == psig.dtype # Check the values if axis == 0: assert np.allclose(grad, 0) else: assert np.allclose(grad, [-1.5, 1, 1, 1, -1.5]) def test_shear_dense(): E = np.eye(3) E_shear = librosa.util.shear(E, factor=1, axis=0) assert np.allclose(E_shear, np.asarray([[1, 0, 0], [0, 0, 1], [0, 1, 0]])) E_shear = librosa.util.shear(E, factor=1, axis=1) assert np.allclose(E_shear, np.asarray([[1, 0, 0], [0, 0, 1], [0, 1, 0]])) E_shear = librosa.util.shear(E, factor=-1, axis=1) assert np.allclose(E_shear, np.asarray([[1, 1, 1], [0, 0, 0], [0, 0, 0]])) E_shear = librosa.util.shear(E, factor=-1, axis=0) assert np.allclose(E_shear, np.asarray([[1, 0, 0], [1, 0, 0], [1, 0, 0]])) @pytest.mark.parametrize("fmt", ["csc", "csr", "lil", "dok"]) def test_shear_sparse(fmt): E = scipy.sparse.eye_array(3, format=fmt) E_shear = librosa.util.shear(E, factor=1, axis=0) assert scipy.sparse.issparse(E_shear) assert not isinstance(E_shear, scipy.sparse.spmatrix) assert isinstance( E_shear, (scipy.sparse.csc_array, scipy.sparse.csr_array, scipy.sparse.lil_array, scipy.sparse.dok_array), ) assert E_shear.format == fmt assert np.allclose(E_shear.toarray(), np.asarray([[1, 0, 0], [0, 0, 1], [0, 1, 0]])) E_shear = librosa.util.shear(E, factor=1, axis=1) assert scipy.sparse.issparse(E_shear) assert not isinstance(E_shear, scipy.sparse.spmatrix) assert isinstance( E_shear, (scipy.sparse.csc_array, scipy.sparse.csr_array, scipy.sparse.lil_array, scipy.sparse.dok_array), ) assert E_shear.format == fmt assert np.allclose(E_shear.toarray(), np.asarray([[1, 0, 0], [0, 0, 1], [0, 1, 0]])) E_shear = librosa.util.shear(E, factor=-1, axis=1) assert scipy.sparse.issparse(E_shear) assert not isinstance(E_shear, scipy.sparse.spmatrix) assert isinstance( E_shear, (scipy.sparse.csc_array, scipy.sparse.csr_array, scipy.sparse.lil_array, scipy.sparse.dok_array), ) assert E_shear.format == fmt assert np.allclose(E_shear.toarray(), np.asarray([[1, 1, 1], [0, 0, 0], [0, 0, 0]])) E_shear = librosa.util.shear(E, factor=-1, axis=0) assert scipy.sparse.issparse(E_shear) assert not isinstance(E_shear, scipy.sparse.spmatrix) assert isinstance( E_shear, (scipy.sparse.csc_array, scipy.sparse.csr_array, scipy.sparse.lil_array, scipy.sparse.dok_array), ) assert E_shear.format == fmt assert np.allclose(E_shear.toarray(), np.asarray([[1, 0, 0], [1, 0, 0], [1, 0, 0]])) @pytest.mark.parametrize("fmt", ["csc", "csr", "lil", "dok"]) def test_shear_sparse_matrix_preserves_type(fmt): E = scipy.sparse.eye(3, format=fmt) # NOTE: matrix constructor E_shear = librosa.util.shear(E, factor=1, axis=0) assert scipy.sparse.issparse(E_shear) assert isinstance(E_shear, scipy.sparse.spmatrix) assert not isinstance(E_shear, scipy.sparse.sparray) assert E_shear.format == fmt # type: ignore[attr-defined] assert np.allclose(E_shear.toarray(), np.asarray([[1,0,0],[0,0,1],[0,1,0]])) # type: ignore[attr-defined] @pytest.mark.xfail(raises=librosa.ParameterError) def test_shear_badfactor(): librosa.util.shear(np.eye(3), factor=None) # type: ignore def test_shear_sparse_1d_raises(): try: x = scipy.sparse.coo_array([1, 0, 2]) except Exception: pytest.skip("SciPy cannot construct a 1D sparse array in this build") if x.ndim != 1: pytest.skip("SciPy coerces 1D sparse array constructors to 2D in this version") with pytest.raises(librosa.ParameterError, match="Input must be 2D"): librosa.util.shear(x, factor=1, axis=0) def test_stack_contig(): x1 = np.ones(3) x2 = -np.ones(3) xs = librosa.util.stack([x1, x2], axis=0) assert xs.flags["F_CONTIGUOUS"] assert np.allclose(xs, [[1, 1, 1], [-1, -1, -1]]) @pytest.mark.xfail(raises=librosa.ParameterError) def test_stack_fail_shape(): x1 = np.ones(3) x2 = np.ones(2) librosa.util.stack([x1, x2]) @pytest.mark.xfail(raises=librosa.ParameterError) def test_stack_fail_empty(): librosa.util.stack([]) @pytest.mark.parametrize("axis", [0, 1, -1]) def test_stack_consistent(axis, rng): x = rng.standard_normal(size=(5, 10, 20)) xs = librosa.util.stack([x, x], axis=axis) xsnp = np.stack([x, x], axis=axis) assert np.allclose(xs, xsnp) if axis != 0: assert xs.flags["C_CONTIGUOUS"] @pytest.mark.network @pytest.mark.parametrize( "key", [ "trumpet", "brahms", "nutcracker", "choice", "drese", "drese2", "humpback", "libri1", "libri2", "libri3", "pistachio", "robin", "snare", "sweetwaltz", "fishin", "vibeace", ], ) @pytest.mark.parametrize("hq", [False, True]) def test_example(key, hq): fn = librosa.example(key, hq=hq) assert os.path.exists(fn) def test_example_url(): url = librosa.example("trumpet", url=True) assert url.startswith("https://") or url.startswith("http://") @pytest.mark.xfail(raises=librosa.ParameterError) def test_example_fail(): librosa.example("no such track") @pytest.mark.network @pytest.mark.parametrize( "key", [ "trumpet", "brahms", "nutcracker", "choice", "drese", "drese2", "humpback", "libri1", "libri2", "libri3", "pistachio", "robin", "snare", "sweetwaltz", "fishin", "vibeace", ], ) def test_example_info(key): librosa.util.example_info(key) @pytest.mark.xfail(raises=librosa.ParameterError) def test_example_info_fail(): librosa.util.example_info("no such track") def test_list_examples(): librosa.util.list_examples() @pytest.mark.parametrize( "dtype,target", [ (np.float32, np.complex64), (np.float64, np.complex128), (np.int32, np.complex64), (np.complex128, np.complex128), ], ) def test_dtype_r2c(dtype, target): inf_type = librosa.util.dtype_r2c(dtype) # better to do a bidirectional subtype test than strict equality here assert np.issubdtype(inf_type, target) and np.issubdtype(target, inf_type) @pytest.mark.parametrize( "dtype,target", [ (np.float32, np.float32), (np.complex64, np.float32), (np.int32, np.float32), (np.complex128, np.float64), (complex, float), (np.dtype(complex), np.float64), ], ) def test_dtype_c2r(dtype, target): inf_type = librosa.util.dtype_c2r(dtype) # better to do a bidirectional subtype test than strict equality here assert np.issubdtype(inf_type, target) and np.issubdtype(target, inf_type) @pytest.mark.xfail(raises=librosa.ParameterError) def test_expand_to_badshape(): x = np.arange(3) librosa.util.expand_to(x, ndim=2, axes=[0, 1]) @pytest.mark.xfail(raises=librosa.ParameterError) def test_expand_to_badndim(): x = np.zeros((3, 3)) librosa.util.expand_to(x, ndim=1, axes=[0, 1]) @pytest.mark.parametrize("axes", [0, 1, -1, [0], [1], [-1]]) @pytest.mark.parametrize("ndim", [2, 3, 4]) def test_expand_to_1d(axes, ndim): x = np.arange(5) xout = librosa.util.expand_to(x, ndim=ndim, axes=axes) assert xout.ndim == ndim assert xout.size == x.size assert np.allclose(x, xout.squeeze()) if not hasattr(axes, "__iter__"): axes = [axes] # Verify that remaining dimensions match assert np.array_equal(x.shape, xout.squeeze().shape) # Verify that we have 1s on expanded dims for i, ax in enumerate(axes): assert xout.shape[ax] == x.shape[i] @pytest.mark.parametrize("axes", [[0, 1], [0, 2], [1, 2]]) @pytest.mark.parametrize("ndim", [3, 4]) def test_expand_to_2d(axes, ndim): x = np.multiply.outer(np.arange(4), np.arange(6)) xout = librosa.util.expand_to(x, ndim=ndim, axes=axes) assert xout.ndim == ndim assert xout.size == x.size assert np.allclose(x, xout.squeeze()) # Verify that remaining dimensions match assert np.array_equal(x.shape, xout.squeeze().shape) # Verify that we have 1s on expanded dims for i, ax in enumerate(axes): assert xout.shape[ax] == x.shape[i] def test_count_unique(): x = np.vander(np.arange(5)) x0 = librosa.util.count_unique(x, axis=0) x1 = librosa.util.count_unique(x, axis=1) assert np.allclose(x0, [5, 5, 5, 5, 1]) assert np.allclose(x1, [2, 1, 5, 5, 5]) def test_is_unique(): x = np.vander(np.arange(5)) x0 = librosa.util.is_unique(x, axis=0) x1 = librosa.util.is_unique(x, axis=1) assert np.allclose(x0, [True, True, True, True, False]) assert np.allclose(x1, [False, False, True, True, True]) @pytest.mark.parametrize("x", [-2, 3, np.arange(-3, 3)]) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) def test_abs2_real(x, dtype): x = dtype(x) p = librosa.util.abs2(x) assert np.allclose(p, x**2) @pytest.mark.parametrize("x", [(2 - 2j), (3 + 0j), (0.5j) ** np.arange(6)]) @pytest.mark.parametrize("dtype", [np.complex64, np.complex128]) def test_abs2_complex(x, dtype): x_cast: Union[np.complexfloating[Any, Any], np.ndarray] = dtype(x) p = librosa.util.abs2(x_cast) assert np.allclose(p, np.abs(x_cast) ** 2) assert p.dtype == librosa.util.dtype_c2r(x_cast.dtype) def test_abs2_int_dtype(): x = np.arange(5, dtype=np.int16) y = librosa.util.abs2(x, dtype=None) assert x.dtype == y.dtype z = librosa.util.abs2(x, dtype=np.float32) assert z.dtype == np.float32 def test_abs2_complex_dtype(): x = np.arange(5, dtype=np.complex64) y = librosa.util.abs2(x, dtype=None) assert np.isrealobj(y) # complex64 -> float32 by default assert y.dtype == np.float32 z = librosa.util.abs2(x, dtype=np.float64) # force it to float64 assert z.dtype == np.float64 @pytest.mark.parametrize("dtype", [np.float32, np.float64]) @pytest.mark.parametrize("angles", [np.pi / 2, [np.pi / 2, -np.pi / 3]]) @pytest.mark.parametrize("mag", [None, 2]) def test_phasor(dtype, angles, mag): angles_cast: Union[np.floating[Any], np.ndarray] = dtype(angles) z = np.exp(1j * angles_cast) if mag is not None: mag = dtype(mag) z *= mag z2 = librosa.util.phasor(angles_cast, mag=mag) assert np.allclose(z, z2) assert z2.dtype == librosa.util.dtype_r2c(dtype) @pytest.mark.network @pytest.mark.parametrize("bib", [False, True]) def test_cite_released(bib): version = "0.10.1" doi = "https://doi.org/10.5281/zenodo.8252662" result = librosa.cite(version=version, bib=bib) if bib: assert "@misc{" == result[:6] assert doi in result else: assert doi == result @pytest.mark.network @pytest.mark.xfail(raises=librosa.ParameterError) def test_cite_badversion(): librosa.cite(version="-1.5") @pytest.mark.network @pytest.mark.xfail(raises=librosa.ParameterError) @pytest.mark.parametrize("bib", [False, True]) def test_cite_unreleased(bib): librosa.cite("0.10.0.dev0", bib=bib) @pytest.mark.parametrize("n_bytes", [1, 2, 4]) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) def test_buf_to_float(n_bytes, dtype): x = np.arange(-10, 10, dtype=f"