././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1774203195.426795 samplerate-0.2.4/0000755000175100017510000000000015160030473013317 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1774203176.0 samplerate-0.2.4/.gitattributes0000644000175100017510000000004415160030450016203 0ustar00runnerrunnersamplerate/_version.py export-subst ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1774203195.4177344 samplerate-0.2.4/.github/0000755000175100017510000000000015160030473014657 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1774203195.4219255 samplerate-0.2.4/.github/workflows/0000755000175100017510000000000015160030473016714 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1774203176.0 samplerate-0.2.4/.github/workflows/pythonpackage.yml0000644000175100017510000001013015160030450022262 0ustar00runnerrunnername: samplerate on: push: branches: [master] tags: ["v*", "test-*"] pull_request: types: [opened, synchronize, reopened, labeled] workflow_dispatch: jobs: build_sdist: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: submodules: recursive fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.9" - name: Build sdist run: | python -m pip install --upgrade pip pip install -U setuptools setuptools_scm build twine python -m build --sdist twine check dist/* - uses: actions/upload-artifact@v6 with: name: sdist path: dist/*.tar.gz build_wheels_pr: if: github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'full-build') runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - os: ubuntu-latest cibw_build: "cp314-manylinux_x86_64" cibw_archs: "x86_64" - os: macos-latest cibw_build: "cp314-macosx_universal2" cibw_archs: "universal2" - os: windows-latest cibw_build: "cp314-win_amd64" cibw_archs: "AMD64" steps: - uses: actions/checkout@v6 with: submodules: recursive fetch-depth: 0 - uses: pypa/cibuildwheel@v3.4.0 env: CIBW_BUILD: ${{ matrix.cibw_build }} CIBW_ARCHS: ${{ matrix.cibw_archs }} CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28 CIBW_TEST_REQUIRES: "pytest numpy" CIBW_TEST_COMMAND: "pytest {project}/tests" build_wheels: if: >- github.event_name == 'push' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'full-build')) runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] steps: - uses: actions/checkout@v6 with: submodules: recursive fetch-depth: 0 - name: Set up QEMU if: runner.os == 'Linux' uses: docker/setup-qemu-action@v3 with: platforms: all - uses: pypa/cibuildwheel@v3.4.0 env: CIBW_BUILD: "cp39-* cp310-* cp311-* cp312-* cp313-* cp314-*" CIBW_SKIP: "*-musllinux_* cp39-*aarch64 cp310-*aarch64" CIBW_ARCHS_LINUX: "x86_64 aarch64" CIBW_ARCHS_MACOS: "universal2" CIBW_ARCHS_WINDOWS: "AMD64" CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28 CIBW_MANYLINUX_AARCH64_IMAGE: manylinux_2_28 CIBW_TEST_REQUIRES: "pytest numpy" CIBW_TEST_COMMAND: "pytest {project}/tests" CIBW_TEST_SKIP: "cp314-*" - uses: actions/upload-artifact@v6 with: name: wheels-${{ matrix.os }} path: wheelhouse/*.whl publish: needs: [build_sdist, build_wheels] runs-on: ubuntu-latest if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v') steps: - uses: actions/download-artifact@v8 with: path: dist merge-multiple: true - name: Validate wheels run: | pip install --upgrade twine packaging twine check dist/* - name: Publish to PyPI env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | twine upload --skip-existing dist/* publish-test: needs: [build_sdist, build_wheels] runs-on: ubuntu-latest if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/test-') steps: - uses: actions/download-artifact@v8 with: path: dist merge-multiple: true - name: Validate wheels run: | pip install --upgrade twine packaging twine check dist/* - name: Publish to PyPI Test env: TWINE_USERNAME: ${{ secrets.PYPITEST_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPITEST_PASSWORD }} TWINE_REPOSITORY_URL: https://test.pypi.org/legacy/ run: | twine upload --skip-existing dist/* ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1774203176.0 samplerate-0.2.4/.gitignore0000644000175100017510000000017015160030450015300 0ustar00runnerrunner__pycache__/ *.py[cod] .coverage .cache/ .eggs/ *.egg-info dist/ build/ docs/_build tags .vscode/ samplerate/_src.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1774203176.0 samplerate-0.2.4/CMakeLists.txt0000644000175100017510000000251415160030450016054 0ustar00runnerrunner# https://stackoverflow.com/questions/51907755/building-a-pybind11-module-with-cpp-and-cuda-sources-using-cmake cmake_minimum_required(VERSION 3.15) set(CMAKE_POLICY_VERSION_MINIMUM 3.5) project(python-samplerate) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(PYBIND11_FINDPYTHON ON) # adds the external dependencies add_subdirectory(external) pybind11_add_module(python-samplerate src/samplerate.cpp) target_include_directories(python-samplerate PRIVATE ./external/libsamplerate/include) if(MSVC) target_compile_options(python-samplerate PRIVATE /EHsc /MP /bigobj) set(CMAKE_EXE_LINKER_FLAGS /MANIFEST:NO) endif() if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID MATCHES "GNU" OR (CMAKE_CXX_COMPILER_ID MATCHES "Intel" AND NOT WIN32)) target_compile_options(python-samplerate PRIVATE -std=c++14 -O3 -Wall -Wextra -fPIC) endif() ### stick the package and libsamplerate version into the module target_compile_definitions(python-samplerate PUBLIC LIBSAMPLERATE_VERSION="${LIBSAMPLERATE_VERSION}" PRIVATE $<$:VERSION_INFO="${PACKAGE_VERSION_INFO}"> ) ### Final target setup set_target_properties( python-samplerate PROPERTIES PREFIX "" OUTPUT_NAME "samplerate" LINKER_LANGUAGE C ) target_link_libraries(python-samplerate PUBLIC samplerate) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1774203176.0 samplerate-0.2.4/LICENSE.rst0000644000175100017510000000211315160030450015123 0ustar00runnerrunnerThe MIT License (MIT) ===================== Copyright © 2017 Tino Wagner Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1774203176.0 samplerate-0.2.4/MANIFEST.in0000644000175100017510000000026515160030450015053 0ustar00runnerrunnerinclude README.rst include LICENSE.rst include versioneer.py include src/*.cpp include CMakeLists.txt include external/CMakeLists.txt include requirements.txt include examples/*.py ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1774203195.4267647 samplerate-0.2.4/PKG-INFO0000644000175100017510000000753215160030473014423 0ustar00runnerrunnerMetadata-Version: 2.4 Name: samplerate Version: 0.2.4 Summary: Monolithic python wrapper for libsamplerate based on pybind11 and NumPy Author-email: Robin Scheibler , Tino Wagner License-Expression: MIT Keywords: samplerate,converter,signal processing,audio Classifier: Development Status :: 3 - Alpha Classifier: Environment :: Console Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Science/Research Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Scientific/Engineering Classifier: Topic :: Multimedia :: Sound/Audio Requires-Python: >=3.7 Description-Content-Type: text/x-rst License-File: LICENSE.rst Requires-Dist: numpy Dynamic: license-file python-samplerate ================= .. image:: https://img.shields.io/pypi/v/samplerate.svg :target: https://pypi.python.org/pypi/samplerate .. image:: https://img.shields.io/pypi/l/samplerate.svg :target: https://pypi.python.org/pypi/samplerate .. image:: https://img.shields.io/pypi/wheel/samplerate.svg :target: https://pypi.python.org/pypi/samplerate .. image:: https://img.shields.io/pypi/pyversions/samplerate.svg :target: https://pypi.python.org/pypi/samplerate .. image:: https://readthedocs.org/projects/python-samplerate/badge/?version=latest :target: http://python-samplerate.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status This is a wrapper around Erik de Castro Lopo's `libsamplerate`_ (aka Secret Rabbit Code) for high-quality sample rate conversion. It implements all three `APIs `_ available in `libsamplerate`_: * **Simple API**: for resampling a large chunk of data with a single library call * **Full API**: for obtaining the resampled signal from successive chunks of data * **Callback API**: like Full API, but input samples are provided by a callback function The `libsamplerate`_ library is statically built together with the python bindings using `pybind11 `_. Installation ------------ $ pip install samplerate Binary wheels of `libsamplerate`_ are available for macOS (x86_64, arm64), Linux (x86_64, aarch64), and Windows (x86_64). Building from source on other platforms requires a C++14 or later compiler. Usage ----- .. code-block:: python import numpy as np import samplerate # Synthesize data fs = 1000. t = np.arange(fs * 2) / fs input_data = np.sin(2 * np.pi * 5 * t) # Simple API ratio = 1.5 converter = 'sinc_best' # or 'sinc_fastest', ... output_data_simple = samplerate.resample(input_data, ratio, converter) # Full API resampler = samplerate.Resampler(converter, channels=1) output_data_full = resampler.process(input_data, ratio, end_of_input=True) # The result is the same for both APIs. assert np.allclose(output_data_simple, output_data_full) # See `samplerate.CallbackResampler` for the Callback API, or # `examples/play_modulation.py` for an example. See ``samplerate.resample``, ``samplerate.Resampler``, and ``samplerate.CallbackResampler`` in the API documentation for details. See also -------- * `scikits.samplerate `_ implements only the Simple API and uses `Cython `_ for extern calls. The `resample` function of `scikits.samplerate` and this package share the same function signature for compatiblity. * `resampy `_: sample rate conversion in Python + Cython. License ------- This project is licensed under the `MIT license `_. As of version 0.1.9, `libsamplerate`_ is licensed under the `2-clause BSD license `_. .. _libsamplerate: http://www.mega-nerd.com/libsamplerate/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1774203176.0 samplerate-0.2.4/README.rst0000644000175100017510000000610615160030450015004 0ustar00runnerrunnerpython-samplerate ================= .. image:: https://img.shields.io/pypi/v/samplerate.svg :target: https://pypi.python.org/pypi/samplerate .. image:: https://img.shields.io/pypi/l/samplerate.svg :target: https://pypi.python.org/pypi/samplerate .. image:: https://img.shields.io/pypi/wheel/samplerate.svg :target: https://pypi.python.org/pypi/samplerate .. image:: https://img.shields.io/pypi/pyversions/samplerate.svg :target: https://pypi.python.org/pypi/samplerate .. image:: https://readthedocs.org/projects/python-samplerate/badge/?version=latest :target: http://python-samplerate.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status This is a wrapper around Erik de Castro Lopo's `libsamplerate`_ (aka Secret Rabbit Code) for high-quality sample rate conversion. It implements all three `APIs `_ available in `libsamplerate`_: * **Simple API**: for resampling a large chunk of data with a single library call * **Full API**: for obtaining the resampled signal from successive chunks of data * **Callback API**: like Full API, but input samples are provided by a callback function The `libsamplerate`_ library is statically built together with the python bindings using `pybind11 `_. Installation ------------ $ pip install samplerate Binary wheels of `libsamplerate`_ are available for macOS (x86_64, arm64), Linux (x86_64, aarch64), and Windows (x86_64). Building from source on other platforms requires a C++14 or later compiler. Usage ----- .. code-block:: python import numpy as np import samplerate # Synthesize data fs = 1000. t = np.arange(fs * 2) / fs input_data = np.sin(2 * np.pi * 5 * t) # Simple API ratio = 1.5 converter = 'sinc_best' # or 'sinc_fastest', ... output_data_simple = samplerate.resample(input_data, ratio, converter) # Full API resampler = samplerate.Resampler(converter, channels=1) output_data_full = resampler.process(input_data, ratio, end_of_input=True) # The result is the same for both APIs. assert np.allclose(output_data_simple, output_data_full) # See `samplerate.CallbackResampler` for the Callback API, or # `examples/play_modulation.py` for an example. See ``samplerate.resample``, ``samplerate.Resampler``, and ``samplerate.CallbackResampler`` in the API documentation for details. See also -------- * `scikits.samplerate `_ implements only the Simple API and uses `Cython `_ for extern calls. The `resample` function of `scikits.samplerate` and this package share the same function signature for compatiblity. * `resampy `_: sample rate conversion in Python + Cython. License ------- This project is licensed under the `MIT license `_. As of version 0.1.9, `libsamplerate`_ is licensed under the `2-clause BSD license `_. .. _libsamplerate: http://www.mega-nerd.com/libsamplerate/ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1774203195.4227548 samplerate-0.2.4/docs/0000755000175100017510000000000015160030473014247 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1774203176.0 samplerate-0.2.4/docs/Makefile0000644000175100017510000000114615160030450015704 0ustar00runnerrunner# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SPHINXPROJ = python-samplerate SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1774203176.0 samplerate-0.2.4/docs/changelog.rst0000644000175100017510000000006715160030450016726 0ustar00runnerrunnerChange Log ########## 0.1.0 ===== * Initial release. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1774203176.0 samplerate-0.2.4/docs/conf.py0000644000175100017510000001264215160030450015546 0ustar00runnerrunner#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # python-samplerate documentation build configuration file, created by # sphinx-quickstart on Thu Feb 9 12:32:39 2017. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) import samplerate # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode", "sphinx.ext.napoleon"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = ".rst" # The master toctree document. master_doc = "index" # General information about the project. project = "python-samplerate" copyright = "2017, Tino Wagner" author = "Tino Wagner" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The full version, including alpha/beta/rc tags. release = samplerate.__version__ # The short X.Y version. def get_short_version(version): """Return short version from PEP-440 compatible version string.""" if "+" in version: return version[: version.find("+")] return version version = get_short_version(release) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = [] # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "alabaster" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = { "description": "Sample rate conversion in Python using libsamplerate", "github_user": "tuxu", "github_repo": "python-samplerate", "github_banner": True, "fixed_sidebar": True, } html_sidebars = { "**": [ "about.html", "navigation.html", "relations.html", "searchbox.html", "donate.html", ] } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. htmlhelp_basename = "python-sampleratedoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ( master_doc, "python-samplerate.tex", "python-samplerate Documentation", "Tino Wagner", "manual", ), ] # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, "python-samplerate", "python-samplerate Documentation", [author], 1) ] # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( master_doc, "python-samplerate", "python-samplerate Documentation", author, "python-samplerate", "One line description of project.", "Miscellaneous", ), ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1774203176.0 samplerate-0.2.4/docs/index.rst0000644000175100017510000000052115160030450016101 0ustar00runnerrunner.. include:: ../README.rst API documentation ----------------- .. toctree:: :maxdepth: 2 samplerate/index Change Log ---------- .. toctree:: :maxdepth: 2 changelog Indices and tables ------------------ * :ref:`genindex` * :ref:`modindex` * :ref:`search` .. _libsamplerate: http://www.mega-nerd.com/libsamplerate/ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1774203195.4235795 samplerate-0.2.4/docs/samplerate/0000755000175100017510000000000015160030473016404 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1774203176.0 samplerate-0.2.4/docs/samplerate/converters.rst0000644000175100017510000000077315160030450021332 0ustar00runnerrunner:mod:`samplerate.converters` -- Sample rate converters ====================================================== .. module:: samplerate.converters Converter types --------------- .. autoclass:: ConverterType :members: :undoc-members: Sample rate converters ---------------------- Simple ^^^^^^ .. autofunction:: resample Full API ^^^^^^^^ .. autoclass:: Resampler :members: :undoc-members: Callback API ^^^^^^^^^^^^ .. autoclass:: CallbackResampler :members: :undoc-members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1774203176.0 samplerate-0.2.4/docs/samplerate/exceptions.rst0000644000175100017510000000032115160030450021306 0ustar00runnerrunner:mod:`samplerate.exceptions` -- Sample rate exceptions ====================================================== .. module:: samplerate.exceptions .. autoclass:: ResamplingError :members: :undoc-members:././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1774203176.0 samplerate-0.2.4/docs/samplerate/index.rst0000644000175100017510000000021515160030450020236 0ustar00runnerrunner`samplerate` module documentation ================================= .. toctree:: :maxdepth: 2 samplerate converters exceptions ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1774203176.0 samplerate-0.2.4/docs/samplerate/samplerate.rst0000644000175100017510000000117415160030450021271 0ustar00runnerrunner:mod:`samplerate` -- Python bindings for libsamplerate based on CFFI and NumPy ============================================================================== .. module:: samplerate Simple API ---------- .. function:: samplerate.resample See :func:`samplerate.converters.resample`. Full API -------- .. class:: samplerate.Resampler See :func:`samplerate.converters.Resampler`. Callback API ------------ .. class:: samplerate.CallbackResampler See :func:`samplerate.converters.CallbackResampler`. Exceptions ---------- .. class:: samplerate.ResamplingError See :func:`samplerate.converters.CallbackResampler`. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1774203195.4237695 samplerate-0.2.4/examples/0000755000175100017510000000000015160030473015135 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1774203176.0 samplerate-0.2.4/examples/play_modulation.py0000644000175100017510000000715015160030450020705 0ustar00runnerrunner#!/usr/bin/env python """Demonstrate realtime audio resampling and playback using the callback API. A carrier frequency is modulated by a sine wave, and the resulting signal is played back on the default sound output. During playback, the modulation signal is generated at source samplerate, then resampled to target samplerate, and mixed onto the carrier. """ from __future__ import division, print_function import numpy as np import sounddevice as sd import samplerate as sr source_samplerate = 3600 target_samplerate = 44100 converter_type = 'sinc_fastest' params = { 'mod_amplitude': 1, # Modulation amplitude (Hz) 'mod_frequency': 1, # Modulation frequency (Hz) 'fm_gain': 20, # FM gain (Hz/Hz) 'output_volume': 0.1, # Output volume 'carrier_frequency': 500, # Carrier frequency (Hz) } def get_input_callback(samplerate, params, num_samples=256): """Return a function that produces samples of a sine. Parameters ---------- samplerate : float The sample rate. params : dict Parameters for FM generation. num_samples : int, optional Number of samples to be generated on each call. """ amplitude = params['mod_amplitude'] frequency = params['mod_frequency'] def producer(): """Generate samples. Yields ------ samples : ndarray A number of samples (`num_samples`) of the sine. """ start_time = 0 while True: time = start_time + np.arange(num_samples) / samplerate start_time += num_samples / samplerate output = amplitude * np.cos(2 * np.pi * frequency * time) yield output return lambda p=producer(): next(p) def get_playback_callback(resampler, samplerate, params): """Return a sound playback callback. Parameters ---------- resampler The resampler from which samples are read. samplerate : float The sample rate. params : dict Parameters for FM generation. """ def callback(outdata, frames, time, _): """Playback callback. Read samples from the resampler and modulate them onto a carrier frequency. """ last_fmphase = getattr(callback, 'last_fmphase', 0) df = params['fm_gain'] * resampler.read(frames) df = np.pad(df, (0, frames - len(df)), mode='constant') t = time.outputBufferDacTime + np.arange(frames) / samplerate phase = 2 * np.pi * params['carrier_frequency'] * t fmphase = last_fmphase + 2 * np.pi * np.cumsum(df) / samplerate outdata[:, 0] = params['output_volume'] * np.cos(phase + fmphase) callback.last_fmphase = fmphase[-1] return callback def main(source_samplerate, target_samplerate, params, converter_type): """Setup the resampling and audio output callbacks and start playback.""" from time import sleep ratio = target_samplerate / source_samplerate with sr.CallbackResampler(get_input_callback(source_samplerate, params), ratio, converter_type) as resampler, \ sd.OutputStream(channels=1, samplerate=target_samplerate, callback=get_playback_callback( resampler, target_samplerate, params)): print("Playing back... Ctrl+C to stop.") try: while True: sleep(1) except KeyboardInterrupt: print("Aborting.") if __name__ == '__main__': main( source_samplerate=source_samplerate, target_samplerate=target_samplerate, params=params, converter_type=converter_type) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1774203195.4239578 samplerate-0.2.4/external/0000755000175100017510000000000015160030473015141 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1774203176.0 samplerate-0.2.4/external/CMakeLists.txt0000644000175100017510000000120215160030450017667 0ustar00runnerrunnerinclude(FetchContent) # pybind11 FetchContent_Declare( pybind11 GIT_REPOSITORY https://github.com/pybind/pybind11 GIT_TAG f5fbe867d2d26e4a0a9177a51f6e568868ad3dc8 # 3.0.1 ) FetchContent_MakeAvailable(pybind11) # libsamplerate set(CMAKE_POSITION_INDEPENDENT_CODE ON) set(BUILD_TESTING OFF CACHE BOOL "Disable libsamplerate test build") FetchContent_Declare( libsamplerate GIT_REPOSITORY https://github.com/libsndfile/libsamplerate GIT_TAG c96f5e3de9c4488f4e6c97f59f5245f22fda22f7 # 0.2.2 ) set(LIBSAMPLERATE_VERSION 0.2.2 CACHE STRING PUBLIC) # <-- update libsamplerate version here FetchContent_MakeAvailable(libsamplerate) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1774203176.0 samplerate-0.2.4/pyproject.toml0000644000175100017510000000164715160030450016236 0ustar00runnerrunner[build-system] requires = ["setuptools>=61", "setuptools_scm[toml]>=6.2", "cmake"] build-backend = "setuptools.build_meta" [project] name="samplerate" dynamic = ["version", "readme"] description="Monolithic python wrapper for libsamplerate based on pybind11 and NumPy" authors=[ {"name" = "Robin Scheibler", "email" ="fakufaku@gmail.com"}, {"name" = "Tino Wagner", "email" ="ich@tinowagner.com"} ] requires-python = ">=3.7" classifiers=[ "Development Status :: 3 - Alpha", "Environment :: Console", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Topic :: Scientific/Engineering", "Topic :: Multimedia :: Sound/Audio", ] keywords=["samplerate", "converter", "signal processing", "audio"] license = "MIT" dependencies = ["numpy"] [tool.setuptools.dynamic] readme = {file = ["README.rst"]} [tool.setuptools_scm] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1774203176.0 samplerate-0.2.4/requirements.txt0000644000175100017510000000002415160030450016572 0ustar00runnerrunnernumpy>=1.7.0 pytest ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1774203195.4271202 samplerate-0.2.4/setup.cfg0000644000175100017510000000004615160030473015140 0ustar00runnerrunner[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1774203176.0 samplerate-0.2.4/setup.py0000644000175100017510000001274115160030450015031 0ustar00runnerrunner# Compile and install the python-samplerate package # # Script based on the cmake_example of pybind11 by Dean Moldovan # https://github.com/pybind/cmake_example import os from pathlib import Path import subprocess import sys from pathlib import Path from setuptools import Extension, setup from setuptools.command.build_ext import build_ext # Convert distutils Windows platform specifiers to CMake -A arguments PLAT_TO_CMAKE = { "win32": "Win32", "win-amd64": "x64", "win-arm32": "ARM", "win-arm64": "ARM64", } # A CMakeExtension needs a sourcedir instead of a file list. # The name must be the _single_ output extension from the CMake build. # If you need multiple extensions, see scikit-build. class CMakeExtension(Extension): def __init__(self, name: str, sourcedir: str = "") -> None: super().__init__(name, sources=[]) self.sourcedir = os.fspath(Path(sourcedir).resolve()) class CMakeBuild(build_ext): def build_extension(self, ext: CMakeExtension) -> None: # Must be in this form due to bug in .resolve() only fixed in Python 3.10+ ext_fullpath = Path.cwd() / self.get_ext_fullpath(ext.name) extdir = ext_fullpath.parent.resolve() # Using this requires trailing slash for auto-detection & inclusion of # auxiliary "native" libs debug = int(os.environ.get("DEBUG", 0)) if self.debug is None else self.debug cfg = "Debug" if debug else "Release" # CMake lets you override the generator - we need to check this. # Can be set with Conda-Build, for example. cmake_generator = os.environ.get("CMAKE_GENERATOR", "") # EXAMPLE_VERSION_INFO shows you how to pass a value into the C++ code # from Python. cmake_args = [ f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={extdir}{os.sep}", f"-DPython_EXECUTABLE={sys.executable}", f"-DCMAKE_BUILD_TYPE={cfg}", # not used on MSVC, but no harm ] build_args = [] # Adding CMake arguments set as environment variable # (needed e.g. to build for ARM OSx on conda-forge) if "CMAKE_ARGS" in os.environ: cmake_args += [item for item in os.environ["CMAKE_ARGS"].split(" ") if item] # In this example, we pass in the version to C++. You might not need to. cmake_args += [f"-DPACKAGE_VERSION_INFO={self.distribution.get_version()}"] if self.compiler.compiler_type != "msvc": # Using Ninja-build since it a) is available as a wheel and b) # multithreads automatically. MSVC would require all variables be # exported for Ninja to pick it up, which is a little tricky to do. # Users can override the generator with CMAKE_GENERATOR in CMake # 3.15+. if not cmake_generator or cmake_generator == "Ninja": try: import ninja ninja_executable_path = Path(ninja.BIN_DIR) / "ninja" cmake_args += [ "-GNinja", f"-DCMAKE_MAKE_PROGRAM:FILEPATH={ninja_executable_path}", ] except ImportError: pass else: # Single config generators are handled "normally" single_config = any(x in cmake_generator for x in {"NMake", "Ninja"}) # CMake allows an arch-in-generator style for backward compatibility contains_arch = any(x in cmake_generator for x in {"ARM", "Win64"}) # Specify the arch if using MSVC generator, but only if it doesn't # contain a backward-compatibility arch spec already in the # generator name. if not single_config and not contains_arch: cmake_args += ["-A", PLAT_TO_CMAKE[self.plat_name]] # Multi-config generators have a different way to specify configs if not single_config: cmake_args += [ f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{cfg.upper()}={extdir}" ] build_args += ["--config", cfg] # When building universal2 wheels, we need to set the architectures for CMake. if "universal2" in self.plat_name: cmake_args += ["-DCMAKE_OSX_ARCHITECTURES=arm64;x86_64"] # Set MACOSX_DEPLOYMENT_TARGET for macOS builds. if ( self.plat_name.startswith("macosx-") and "MACOSX_DEPLOYMENT_TARGET" not in os.environ ): target_version = self.plat_name.split("-")[1] os.environ["MACOSX_DEPLOYMENT_TARGET"] = target_version # Set CMAKE_BUILD_PARALLEL_LEVEL to control the parallel build level # across all generators. if "CMAKE_BUILD_PARALLEL_LEVEL" not in os.environ: # self.parallel is a Python 3 only way to set parallel jobs by hand # using -j in the build_ext call, not supported by pip or PyPA-build. if hasattr(self, "parallel") and self.parallel: # CMake 3.12+ only. build_args += [f"-j{self.parallel}"] build_temp = Path(self.build_temp) / ext.name if not build_temp.exists(): build_temp.mkdir(parents=True) subprocess.run( ["cmake", ext.sourcedir, *cmake_args], cwd=build_temp, check=True ) subprocess.run( ["cmake", "--build", ".", *build_args], cwd=build_temp, check=True ) setup( cmdclass={"build_ext": CMakeBuild}, ext_modules=[CMakeExtension("samplerate")], ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1774203195.4241462 samplerate-0.2.4/src/0000755000175100017510000000000015160030473014106 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1774203176.0 samplerate-0.2.4/src/samplerate.cpp0000644000175100017510000005130015160030450016741 0ustar00runnerrunner/* * Python bindings for libsamplerate * Copyright (C) 2023 Robin Scheibler * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * * You should have received a copy of the MIT License along with this program. * If not, see . */ #include #include #include #include #include #include #include #include #include #include #ifndef VERSION_INFO #define VERSION_INFO "nightly" #endif namespace py = pybind11; using namespace pybind11::literals; using callback_t = std::function( void)>; using np_array_f32 = py::array_t; namespace samplerate { enum class ConverterType { sinc_best, sinc_medium, sinc_fastest, zero_order_hold, linear }; class ResamplingException : public std::exception { public: explicit ResamplingException(int err_num) : message{src_strerror(err_num)} {} const char *what() const noexcept override { return message.c_str(); } private: std::string message = ""; }; int get_converter_type(const py::object &obj) { if (py::isinstance(obj)) { py::str py_s = obj; std::string s = static_cast(py_s); if (s.compare("sinc_best") == 0) { return 0; } else if (s.compare("sinc_medium") == 0) { return 1; } else if (s.compare("sinc_fastest") == 0) { return 2; } else if (s.compare("zero_order_hold") == 0) { return 3; } else if (s.compare("linear") == 0) { return 4; } } else if (py::isinstance(obj)) { py::int_ val = obj; return static_cast(val); } else if (py::isinstance(obj)) { py::int_ c = obj.attr("value"); return static_cast(c); } throw std::domain_error("Unsupported converter type"); return -1; } void error_handler(int errnum) { if (errnum > 0 && errnum < 24) { throw ResamplingException(errnum); } else if (errnum != 0) { // the zero case is excluded as it is not an error // this will throw a segmentation fault if we call src_strerror here // also, these should never happen throw std::runtime_error("libsamplerate raised an unknown error code"); } } class Resampler { private: SRC_STATE *_state = nullptr; public: int _converter_type = 0; int _channels = 0; public: Resampler(const py::object &converter_type, int channels) : _converter_type(get_converter_type(converter_type)), _channels(channels) { int _err_num = 0; _state = src_new(_converter_type, _channels, &_err_num); error_handler(_err_num); } // copy constructor Resampler(const Resampler &r) : _converter_type(r._converter_type), _channels(r._channels) { int _err_num = 0; _state = src_clone(r._state, &_err_num); error_handler(_err_num); } // move constructor Resampler(Resampler &&r) : _state(r._state), _converter_type(r._converter_type), _channels(r._channels) { r._state = nullptr; r._converter_type = 0; r._channels = 0; } ~Resampler() { src_delete(_state); } // src_delete handles nullptr case py::array_t process( py::array_t input, double sr_ratio, bool end_of_input) { // accessors for the arrays py::buffer_info inbuf = input.request(); // set the number of channels int channels = 1; if (inbuf.ndim == 2) channels = inbuf.shape[1]; else if (inbuf.ndim > 2) throw std::domain_error("Input array should have at most 2 dimensions"); if (channels != _channels || channels == 0) throw std::domain_error("Invalid number of channels in input data."); const auto new_size = static_cast(std::ceil(inbuf.shape[0] * sr_ratio)); // allocate output array std::vector out_shape{new_size}; if (inbuf.ndim == 2) out_shape.push_back(static_cast(channels)); auto output = py::array_t(out_shape); py::buffer_info outbuf = output.request(); // libsamplerate struct SRC_DATA src_data = { static_cast(inbuf.ptr), // data_in static_cast(outbuf.ptr), // data_out inbuf.shape[0], // input_frames long(new_size), // output_frames 0, // input_frames_used, filled by libsamplerate 0, // output_frames_gen, filled by libsamplerate end_of_input, // end_of_input, not used by src_simple ? sr_ratio // src_ratio, sampling rate conversion ratio }; error_handler(src_process(_state, &src_data)); // create a shorter view of the array if ((size_t)src_data.output_frames_gen < new_size) { out_shape[0] = src_data.output_frames_gen; return py::array_t( out_shape, outbuf.strides, static_cast(outbuf.ptr), output); } return output; } void set_ratio(double new_ratio) { error_handler(src_set_ratio(_state, new_ratio)); } void reset() { error_handler(src_reset(_state)); } Resampler clone() const { return Resampler(*this); } }; namespace { long the_callback_func(void *cb_data, float **data); } // namespace class CallbackResampler { private: SRC_STATE *_state = nullptr; callback_t _callback = nullptr; np_array_f32 _current_buffer; size_t _buffer_ndim = 0; public: double _ratio = 0.0; int _converter_type = 0; size_t _channels = 0; private: void _create() { int _err_num = 0; _state = src_callback_new(the_callback_func, _converter_type, (int)_channels, &_err_num, static_cast(this)); if (_state == nullptr) error_handler(_err_num); } void _destroy() { if (_state != nullptr) { src_delete(_state); _state = nullptr; } } public: CallbackResampler(const callback_t &callback_func, double ratio, const py::object &converter_type, size_t channels) : _callback(callback_func), _ratio(ratio), _converter_type(get_converter_type(converter_type)), _channels(channels) { _create(); } // copy constructor CallbackResampler(const CallbackResampler &r) : _callback(r._callback), _ratio(r._ratio), _converter_type(r._converter_type), _channels(r._channels) { int _err_num = 0; _state = src_clone(r._state, &_err_num); if (_state == nullptr) error_handler(_err_num); } // move constructor CallbackResampler(CallbackResampler &&r) : _state(r._state), _callback(r._callback), _current_buffer(std::move(r._current_buffer)), _buffer_ndim(r._buffer_ndim), _ratio(r._ratio), _converter_type(r._converter_type), _channels(r._channels) { r._state = nullptr; r._callback = nullptr; r._buffer_ndim = 0; r._ratio = 0.0; r._converter_type = 0; r._channels = 0; } ~CallbackResampler() { _destroy(); } void set_buffer(const np_array_f32 &new_buf) { _current_buffer = new_buf; } size_t get_channels() { return _channels; } np_array_f32 callback(void) { auto input = _callback(); auto inbuf = input.request(); if (_buffer_ndim == 0) _buffer_ndim = inbuf.ndim; _current_buffer = input; return input; } py::array_t read(size_t frames) { // allocate output array std::vector out_shape{frames, _channels}; auto output = py::array_t(out_shape); py::buffer_info outbuf = output.request(); if (_state == nullptr) _create(); // read from the callback size_t output_frames_gen = src_callback_read( _state, _ratio, (long)frames, static_cast(outbuf.ptr)); // check error status if (output_frames_gen == 0) { error_handler(src_error(_state)); } // if there is only one channel and the input array had only on dimension // we also output a 1D array if (_channels == 1 && _buffer_ndim == 1) { out_shape.pop_back(); output = py::array_t( out_shape, static_cast(outbuf.ptr)); } // create a shorter view of the array if (output_frames_gen < frames) { out_shape[0] = output_frames_gen; auto strides = std::vector(output.strides(), output.strides() + output.ndim()); return py::array_t( out_shape, strides, static_cast(outbuf.ptr), output); } return output; } void set_starting_ratio(double new_ratio) { error_handler(src_set_ratio(_state, new_ratio)); _ratio = new_ratio; } void reset() { error_handler(src_reset(_state)); } CallbackResampler clone() const { return CallbackResampler(*this); } CallbackResampler &__enter__() { return *this; } void __exit__(const py::object &/*exc_type*/, const py::object &/*exc*/, const py::object &/*exc_tb*/) { _destroy(); } }; namespace { long the_callback_func(void *cb_data, float **data) { CallbackResampler *cb = static_cast(cb_data); int cb_channels = cb->get_channels(); // get the data as a numpy array auto input = cb->callback(); // accessors for the arrays py::buffer_info inbuf = input.request(); // end of stream is signaled by a None, which is cast to a ndarray with ndim // == 0 if (inbuf.ndim == 0) return 0; // set the number of channels int channels = 1; if (inbuf.ndim == 2) channels = inbuf.shape[1]; else if (inbuf.ndim > 2) throw std::domain_error("Input array should have at most 2 dimensions"); if (channels != cb_channels || channels == 0) throw std::domain_error("Invalid number of channels in input data."); *data = static_cast(inbuf.ptr); return (long)inbuf.shape[0]; } } // namespace py::array_t resample( const py::array_t &input, double sr_ratio, const py::object &converter_type, bool verbose) { // input array has shape (n_samples, n_channels) int converter_type_int = get_converter_type(converter_type); // accessors for the arrays py::buffer_info inbuf = input.request(); // set the number of channels int channels = 1; if (inbuf.ndim == 2) channels = inbuf.shape[1]; else if (inbuf.ndim > 2) throw std::domain_error("Input array should have at most 2 dimensions"); if (channels == 0) throw std::domain_error("Invalid number of channels (0) in input data."); const auto new_size = static_cast(std::ceil(inbuf.shape[0] * sr_ratio)); // allocate output array std::vector out_shape{new_size}; if (inbuf.ndim == 2) out_shape.push_back(static_cast(channels)); auto output = py::array_t(out_shape); py::buffer_info outbuf = output.request(); // libsamplerate struct SRC_DATA src_data = { static_cast(inbuf.ptr), // data_in static_cast(outbuf.ptr), // data_out inbuf.shape[0], // input_frames long(new_size), // output_frames 0, // input_frames_used, filled by libsamplerate 0, // output_frames_gen, filled by libsamplerate 0, // end_of_input, not used by src_simple ? sr_ratio // src_ratio, sampling rate conversion ratio }; int ret_code = src_simple(&src_data, converter_type_int, channels); error_handler(ret_code); // create a shorter view of the array if ((size_t)src_data.output_frames_gen < new_size) { out_shape[0] = src_data.output_frames_gen; auto base = output; output = py::array_t( out_shape, outbuf.strides, static_cast(outbuf.ptr), base); } if (verbose) { py::print("samplerate info:"); py::print(src_data.input_frames_used, " input frames used"); py::print(src_data.output_frames_gen, " output frames generated"); } return output; } } // namespace samplerate namespace sr = samplerate; PYBIND11_MODULE(samplerate, m) { m.doc() = "A simple python wrapper library around libsamplerate"; // optional // module // docstring m.attr("__version__") = VERSION_INFO; m.attr("__libsamplerate_version__") = LIBSAMPLERATE_VERSION; auto m_exceptions = m.def_submodule( "exceptions", "Sub-module containing sampling exceptions"); auto m_converters = m.def_submodule( "converters", "Sub-module containing the samplerate converters"); auto m_internals = m.def_submodule("_internals", "Internal helper functions"); // give access to this function for testing m_internals.def( "get_converter_type", &sr::get_converter_type, "Convert python object to integer of converter tpe or raise an error " "if illegal"); m_internals.def( "error_handler", &sr::error_handler, "A function to translate libsamplerate error codes into exceptions"); py::register_exception( m_exceptions, "ResamplingError", PyExc_RuntimeError); m_converters.def("resample", &sr::resample, R"mydelimiter( Resample the signal in `input_data` at once. Parameters ---------- input_data : ndarray Input data. Input data with one or more channels is represented as a 2D array of shape (`num_frames`, `num_channels`). A single channel can be provided as a 1D array of `num_frames` length. For use with `libsamplerate`, `input_data` is converted to 32-bit float and C (row-major) memory order. ratio : float Conversion ratio = output sample rate / input sample rate. converter_type : ConverterType, str, or int Sample rate converter (default: `sinc_best`). verbose : bool If `True`, print additional information about the conversion. Returns ------- output_data : ndarray Resampled input data. Note ---- If samples are to be processed in chunks, `Resampler` and `CallbackResampler` will provide better results and allow for variable conversion ratios. )mydelimiter", "input"_a, "ratio"_a, "converter_type"_a = "sinc_best", "verbose"_a = false); py::class_(m_converters, "Resampler", R"mydelimiter( Resampler. Parameters ---------- converter_type : ConverterType, str, or int Sample rate converter (default: `sinc_best`). num_channels : int Number of channels. )mydelimiter") .def(py::init(), "converter_type"_a = "sinc_best", "channels"_a = 1) .def(py::init()) .def("process", &sr::Resampler::process, R"mydelimiter( Resample the signal in `input_data`. Parameters ---------- input_data : ndarray Input data. Input data with one or more channels is represented as a 2D array of shape (`num_frames`, `num_channels`). A single channel can be provided as a 1D array of `num_frames` length. For use with `libsamplerate`, `input_data` is converted to 32-bit float and C (row-major) memory order. ratio : float Conversion ratio = output sample rate / input sample rate. end_of_input : int Set to `True` if no more data is available, or to `False` otherwise. verbose : bool If `True`, print additional information about the conversion. Returns ------- output_data : ndarray Resampled input data. )mydelimiter", "input"_a, "ratio"_a, "end_of_input"_a = false) .def("reset", &sr::Resampler::reset, "Reset internal state.") .def("set_ratio", &sr::Resampler::set_ratio, "Set a new conversion ratio immediately.") .def("clone", &sr::Resampler::clone, "Creates a copy of the resampler object with the same internal " "state.") .def_readonly("converter_type", &sr::Resampler::_converter_type, "Converter type.") .def_readonly("channels", &sr::Resampler::_channels, "Number of channels."); py::class_(m_converters, "CallbackResampler", R"mydelimiter( CallbackResampler. Parameters ---------- callback : function Function that returns new frames on each call, or `None` otherwise. Input data with one or more channels is represented as a 2D array of shape (`num_frames`, `num_channels`). A single channel can be provided as a 1D array of `num_frames` length. For use with `libsamplerate`, `input_data` is converted to 32-bit float and C (row-major) memory order. ratio : float Conversion ratio = output sample rate / input sample rate. converter_type : ConverterType, str, or int Sample rate converter. channels : int Number of channels. )mydelimiter") .def(py::init(), "callback"_a, "ratio"_a, "converter_type"_a = "sinc_best", "channels"_a = 1) .def(py::init()) .def("read", &sr::CallbackResampler::read, R"mydelimiter( Read a number of frames from the resampler. Parameters ---------- num_frames : int Number of frames to read. Returns ------- output_data : ndarray Resampled frames as a (`num_output_frames`, `num_channels`) or (`num_output_frames`,) array. Note that this may return fewer frames than requested, for example when no more input is available. )mydelimiter", "num_frames"_a) .def("reset", &sr::CallbackResampler::reset, "Reset state.") .def("set_starting_ratio", &sr::CallbackResampler::set_starting_ratio, "Set the starting conversion ratio for the next `read` call.") .def("clone", &sr::CallbackResampler::clone, "Create a copy of the resampler object.") .def("__enter__", &sr::CallbackResampler::__enter__, py::return_value_policy::reference_internal) .def("__exit__", &sr::CallbackResampler::__exit__) .def_readwrite( "ratio", &sr::CallbackResampler::_ratio, "Conversion ratio = output sample rate / input sample rate.") .def_readonly("converter_type", &sr::CallbackResampler::_converter_type, "Converter type.") .def_readonly("channels", &sr::CallbackResampler::_channels, "Number of channels."); py::enum_(m_converters, "ConverterType", R"mydelimiter( Enum of samplerate converter types. Pass any of the members, or their string or value representation, as ``converter_type`` in the resamplers. )mydelimiter") .value("sinc_best", sr::ConverterType::sinc_best) .value("sinc_medium", sr::ConverterType::sinc_medium) .value("sinc_fastest", sr::ConverterType::sinc_fastest) .value("zero_order_hold", sr::ConverterType::zero_order_hold) .value("linear", sr::ConverterType::linear) .export_values(); // Convenience imports m.attr("ResamplingError") = m_exceptions.attr("ResamplingError"); m.attr("resample") = m_converters.attr("resample"); m.attr("CallbackResampler") = m_converters.attr("CallbackResampler"); m.attr("Resampler") = m_converters.attr("Resampler"); m.attr("ConverterType") = m_converters.attr("ConverterType"); } ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1774203195.426176 samplerate-0.2.4/src/samplerate.egg-info/0000755000175100017510000000000015160030473017735 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1774203195.0 samplerate-0.2.4/src/samplerate.egg-info/PKG-INFO0000644000175100017510000000753215160030473021041 0ustar00runnerrunnerMetadata-Version: 2.4 Name: samplerate Version: 0.2.4 Summary: Monolithic python wrapper for libsamplerate based on pybind11 and NumPy Author-email: Robin Scheibler , Tino Wagner License-Expression: MIT Keywords: samplerate,converter,signal processing,audio Classifier: Development Status :: 3 - Alpha Classifier: Environment :: Console Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Science/Research Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Scientific/Engineering Classifier: Topic :: Multimedia :: Sound/Audio Requires-Python: >=3.7 Description-Content-Type: text/x-rst License-File: LICENSE.rst Requires-Dist: numpy Dynamic: license-file python-samplerate ================= .. image:: https://img.shields.io/pypi/v/samplerate.svg :target: https://pypi.python.org/pypi/samplerate .. image:: https://img.shields.io/pypi/l/samplerate.svg :target: https://pypi.python.org/pypi/samplerate .. image:: https://img.shields.io/pypi/wheel/samplerate.svg :target: https://pypi.python.org/pypi/samplerate .. image:: https://img.shields.io/pypi/pyversions/samplerate.svg :target: https://pypi.python.org/pypi/samplerate .. image:: https://readthedocs.org/projects/python-samplerate/badge/?version=latest :target: http://python-samplerate.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status This is a wrapper around Erik de Castro Lopo's `libsamplerate`_ (aka Secret Rabbit Code) for high-quality sample rate conversion. It implements all three `APIs `_ available in `libsamplerate`_: * **Simple API**: for resampling a large chunk of data with a single library call * **Full API**: for obtaining the resampled signal from successive chunks of data * **Callback API**: like Full API, but input samples are provided by a callback function The `libsamplerate`_ library is statically built together with the python bindings using `pybind11 `_. Installation ------------ $ pip install samplerate Binary wheels of `libsamplerate`_ are available for macOS (x86_64, arm64), Linux (x86_64, aarch64), and Windows (x86_64). Building from source on other platforms requires a C++14 or later compiler. Usage ----- .. code-block:: python import numpy as np import samplerate # Synthesize data fs = 1000. t = np.arange(fs * 2) / fs input_data = np.sin(2 * np.pi * 5 * t) # Simple API ratio = 1.5 converter = 'sinc_best' # or 'sinc_fastest', ... output_data_simple = samplerate.resample(input_data, ratio, converter) # Full API resampler = samplerate.Resampler(converter, channels=1) output_data_full = resampler.process(input_data, ratio, end_of_input=True) # The result is the same for both APIs. assert np.allclose(output_data_simple, output_data_full) # See `samplerate.CallbackResampler` for the Callback API, or # `examples/play_modulation.py` for an example. See ``samplerate.resample``, ``samplerate.Resampler``, and ``samplerate.CallbackResampler`` in the API documentation for details. See also -------- * `scikits.samplerate `_ implements only the Simple API and uses `Cython `_ for extern calls. The `resample` function of `scikits.samplerate` and this package share the same function signature for compatiblity. * `resampy `_: sample rate conversion in Python + Cython. License ------- This project is licensed under the `MIT license `_. As of version 0.1.9, `libsamplerate`_ is licensed under the `2-clause BSD license `_. .. _libsamplerate: http://www.mega-nerd.com/libsamplerate/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1774203195.0 samplerate-0.2.4/src/samplerate.egg-info/SOURCES.txt0000644000175100017510000000125015160030473021617 0ustar00runnerrunner.gitattributes .gitignore CMakeLists.txt LICENSE.rst MANIFEST.in README.rst pyproject.toml requirements.txt setup.py .github/workflows/pythonpackage.yml docs/Makefile docs/changelog.rst docs/conf.py docs/index.rst docs/samplerate/converters.rst docs/samplerate/exceptions.rst docs/samplerate/index.rst docs/samplerate/samplerate.rst examples/play_modulation.py external/CMakeLists.txt src/samplerate.cpp src/samplerate.egg-info/PKG-INFO src/samplerate.egg-info/SOURCES.txt src/samplerate.egg-info/dependency_links.txt src/samplerate.egg-info/requires.txt src/samplerate.egg-info/top_level.txt tests/test_api.py tests/test_exception.py tests/test_resampling.py tests/test_resize.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1774203195.0 samplerate-0.2.4/src/samplerate.egg-info/dependency_links.txt0000644000175100017510000000000115160030473024003 0ustar00runnerrunner ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1774203195.0 samplerate-0.2.4/src/samplerate.egg-info/requires.txt0000644000175100017510000000000615160030473022331 0ustar00runnerrunnernumpy ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1774203195.0 samplerate-0.2.4/src/samplerate.egg-info/top_level.txt0000644000175100017510000000001315160030473022461 0ustar00runnerrunnersamplerate ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1774203195.4259884 samplerate-0.2.4/tests/0000755000175100017510000000000015160030473014461 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1774203176.0 samplerate-0.2.4/tests/test_api.py0000644000175100017510000001060515160030450016640 0ustar00runnerrunnerimport numpy as np import pytest import samplerate def test_aliases(): from samplerate.converters import ( Resampler, CallbackResampler, resample, ConverterType, ) from samplerate import ( Resampler, CallbackResampler, resample, ConverterType, ResamplingError, ) from samplerate.exceptions import ResamplingError @pytest.fixture(scope="module", params=[1, 2]) def data(request): num_channels = request.param periods = np.linspace(0, 10, 1000) input_data = [ np.sin(2 * np.pi * periods + i * np.pi / 2) for i in range(num_channels) ] return ( (num_channels, input_data[0]) if num_channels == 1 else (num_channels, np.transpose(input_data)) ) @pytest.fixture(params=[0, 1, 2, 3, 4]) def converter_type(request): return request.param def test_simple(data, converter_type, ratio=2.0): _, input_data = data samplerate.resample(input_data, ratio, converter_type) def test_process(data, converter_type, ratio=2.0): num_channels, input_data = data src = samplerate.Resampler(converter_type, num_channels) src.process(input_data, ratio) def test_match(data, converter_type, ratio=2.0): num_channels, input_data = data output_simple = samplerate.resample(input_data, ratio, converter_type) resampler = samplerate.Resampler(converter_type, channels=num_channels) output_full = resampler.process(input_data, ratio, end_of_input=True) assert np.allclose(output_simple, output_full) def test_callback(data, converter_type, ratio=2.0): _, input_data = data def producer(): yield input_data while True: yield None callback = lambda p=producer(): next(p) channels = input_data.shape[-1] if input_data.ndim == 2 else 1 resampler = samplerate.CallbackResampler(callback, ratio, converter_type, channels) resampler.read(int(ratio) * input_data.shape[0]) def test_callback_with(data, converter_type, ratio=2.0): from samplerate import CallbackResampler _, input_data = data def producer(): yield input_data while True: yield None callback = lambda p=producer(): next(p) channels = input_data.shape[-1] if input_data.ndim == 2 else 1 with CallbackResampler( callback, ratio, converter_type, channels=channels ) as resampler: resampler.read(int(ratio) * input_data.shape[0]) def test_callback_with_2x(data, converter_type, ratio=2.0): """ Tests that there are no errors if we reuse an object created with a context manager """ from samplerate import CallbackResampler _, input_data = data def producer(): yield input_data while True: yield None channels = input_data.shape[-1] if input_data.ndim == 2 else 1 callback = lambda p=producer(): next(p) with CallbackResampler( callback, ratio, converter_type, channels=channels ) as resampler: resampler.read(int(ratio) * input_data.shape[0] // 2) # re-initialize the data producer resampler.read(int(ratio) * input_data.shape[0] // 2) def test_Resampler_clone(): resampler = samplerate.Resampler("sinc_best", 1) new_resampler = resampler.clone() def test_CallbackResampler_clone(data, converter_type, ratio=2.0): _, input_data = data def producer(): yield input_data while True: yield None callback = lambda p=producer(): next(p) channels = input_data.shape[-1] if input_data.ndim == 2 else 1 resampler = samplerate.CallbackResampler(callback, ratio, converter_type, channels) resampler.read(int(ratio) * input_data.shape[0]) new_resampler = resampler.clone() @pytest.mark.parametrize( "input_obj,expected_type", [ (0, 0), (1, 1), (2, 2), (3, 3), (4, 4), ("sinc_best", 0), ("sinc_medium", 1), ("sinc_fastest", 2), ("zero_order_hold", 3), ("linear", 4), (samplerate.ConverterType.sinc_best, 0), (samplerate.ConverterType.sinc_medium, 1), (samplerate.ConverterType.sinc_fastest, 2), (samplerate.ConverterType.zero_order_hold, 3), (samplerate.ConverterType.linear, 4), ], ) def test_converter_type(input_obj, expected_type): ret = samplerate._internals.get_converter_type(input_obj) assert ret == expected_type ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1774203176.0 samplerate-0.2.4/tests/test_exception.py0000644000175100017510000000670115160030450020067 0ustar00runnerrunnerimport numpy as np import pytest import samplerate @pytest.mark.parametrize("errnum", list(range(1, 25))) def test_resampling_error(errnum): if 0 < errnum < 24: with pytest.raises(samplerate.ResamplingError): samplerate._internals.error_handler(errnum) elif errnum != 0: with pytest.raises(RuntimeError): samplerate._internals.error_handler(errnum) def test_unknown_converter_type(): with pytest.raises(ValueError): samplerate._internals.get_converter_type("super-downsampling") def test_resample_zero_channel_input(): data = np.zeros((16000, 0), dtype=np.float32) # does not produce an error, the output shape is (0, 0) # because the number of samples converted is zero with pytest.raises(ValueError): samplerate.resample(data, 0.5, "sinc_fastest") def test_resampler_zero_channel_input(): data = np.zeros((16000, 0), dtype=np.float32) # does not produce an error, the output shape is (0, 0) # because the number of samples converted is zero resampler = samplerate.Resampler("sinc_fastest", 1) with pytest.raises(ValueError): resampler.process(data, 0.5) def test_resample_zero_len_input(): data = np.zeros((0, 1), dtype=np.float32) # does not produce an error, the output shape is (0, 1) # same as the input samplerate.resample(data, 0.5, "sinc_fastest") def test_resample_ndim_too_big(): data = np.zeros((16000, 1, 1), dtype=np.float32) with pytest.raises(ValueError): # fails because the input has 3 dimensions samplerate.resample(data, 0.5, "sinc_fastest") def test_resampler_ndim_too_big(): data = np.zeros((16000, 1, 1), dtype=np.float32) resampler = samplerate.Resampler("sinc_fastest", 1) with pytest.raises(ValueError): # fails because the input has 3 dimensions resampler.process(data, 0.5) def test_resampler_incorrect_channel_number(): data = np.zeros((16000, 2), dtype=np.float32) resampler = samplerate.Resampler("sinc_fastest", 1) with pytest.raises(ValueError): # fails because we defined the converter for 1 channel resampler.process(data, 0.5) def test_callback_resampler_ndim_too_big(): data = np.zeros((16000, 1, 1), dtype=np.float32) def producer(): yield data while True: yield None callback = lambda p=producer(): next(p) cb_resampler = samplerate.CallbackResampler(callback, 0.5, "sinc_fastest", 1) with pytest.raises(ValueError): # fails because the input has 3 dimensions cb_resampler.read(len(data)) def test_callback_resampler_incorrect_channel_number(): data = np.zeros((16000, 2), dtype=np.float32) def producer(): yield data while True: yield None callback = lambda p=producer(): next(p) cb_resampler = samplerate.CallbackResampler(callback, 0.5, "sinc_fastest", 1) with pytest.raises(ValueError): # fails because we defined the converter for 1 channel cb_resampler.read(len(data)) def test_callback_resampler_zero_channels(): data = np.zeros((16000, 0), dtype=np.float32) def producer(): yield data while True: yield None callback = lambda p=producer(): next(p) cb_resampler = samplerate.CallbackResampler(callback, 0.5, "sinc_fastest", 1) with pytest.raises(ValueError): # fails because we defined the converter for 1 channel cb_resampler.read(len(data)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1774203176.0 samplerate-0.2.4/tests/test_resampling.py0000644000175100017510000000607115160030450020232 0ustar00runnerrunnerimport numpy as np import pytest import samplerate def make_tone(freq, sr, duration): t = np.arange(int(sr * duration), dtype=np.float64) / sr return np.sin(2 * np.pi * freq * t), t def window(signal, n_win): """window the signal at beginning and end with window of size n_win/2""" win = np.hanning(2 * n_win) sig_copy = signal.copy() sig_copy[:n_win] *= win[:n_win] sig_copy[-n_win:] *= win[-n_win:] return sig_copy def make_sweep(T, fs, f_lo=0.0, f_hi=None, fade=None, ascending=False): """ Exponential sine sweep Parameters ---------- T: float length in seconds fs: sampling frequency f_lo: float lowest frequency in fraction of fs (default 0) f_hi: float lowest frequency in fraction of fs (default 1) fade: float, optional length of fade in and out in seconds (default 0) ascending: bool, optional """ if f_hi is None: f_hi = fs / 2 elif f_hi < 0.0: f_hi = fs / 2 + f_hi elif f_hi > fs / 2: f_hi = fs / 2 if f_lo < 1.0: f_lo = 1.0 if f_lo > f_hi: raise ValueError("Error: need 0. <= f_lo < f_hi <= fs/2") Ts = 1.0 / fs # Sampling period in [s] N = np.floor(T / Ts) # number of samples n = np.arange(0, N, dtype="float64") # Sample index om1 = 2 * np.pi * f_lo om2 = 2 * np.pi * f_hi sweep = np.sin( om1 * N * Ts / np.log(om2 / om1) * (np.exp(n / N * np.log(om2 / om1)) - 1) ) if not ascending: sweep = sweep[::-1] if fade is not None and fade > 0.0: sweep = window(sweep, int(fs * fade)) return sweep, np.arange(sweep.shape[0]) / fs @pytest.mark.parametrize("sr_orig,sr_new", [(44100, 22050), (22050, 44100)]) @pytest.mark.parametrize( "fil,rms", [ (samplerate.ConverterType.sinc_best, 1e-6), (samplerate.ConverterType.sinc_medium, 1e-5), (samplerate.ConverterType.sinc_fastest, 1e-4), ], ) def test_quality_sine(sr_orig, sr_new, fil, rms): FREQ = 512.0 DURATION = 2.0 x, _ = make_tone(FREQ, sr_orig, DURATION) y, _ = make_tone(FREQ, sr_new, DURATION) y_pred = samplerate.resample(x, sr_new / sr_orig, fil) idx = slice(sr_new // 2, -sr_new // 2) err = np.mean(np.abs(y[idx] - y_pred[idx])) assert err <= rms, "{:g} > {:g}".format(err, rms) @pytest.mark.parametrize("sr_orig,sr_new", [(44100, 22050), (22050, 44100)]) @pytest.mark.parametrize( "fil,rms", [ (samplerate.ConverterType.sinc_best, 1e-6), (samplerate.ConverterType.sinc_medium, 1e-5), (samplerate.ConverterType.sinc_fastest, 1e-4), ], ) def test_quality_sweep(sr_orig, sr_new, fil, rms): FREQ = 8000 DURATION = 5.0 x, _ = make_sweep(DURATION, sr_orig, 0.0, FREQ, fade=0.1) y, _ = make_sweep(DURATION, sr_new, 0.0, FREQ, fade=0.1) x = x[::-1] y = y[::-1] y_pred = samplerate.resample(x, sr_new / sr_orig, fil) idx = slice(sr_new // 2, -sr_new // 2) err = np.mean(np.abs(y[idx] - y_pred[idx])) assert err <= rms, "{:g} > {:g}".format(err, rms) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1774203176.0 samplerate-0.2.4/tests/test_resize.py0000644000175100017510000000047315160030450017372 0ustar00runnerrunnerimport numpy as np import samplerate def test_resize(): np.random.seed(0) ratio = 0.9 x = np.random.randn(167) # internally, the bindings will first prepare a buffer of size # ceil(167 * 0.9) = 151, which will be resized to 150 y = samplerate.resample(x, 0.9) assert y.shape[0] == 150