diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a01e2fe8a..19072a523 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ We'd love to get patches from you! To compile Pedalboard from scratch, the following packages will need to be installed: -- [Python 3.8](https://www.python.org/downloads/) or higher. +- [Python 3.10](https://www.python.org/downloads/) or higher. - A C++ compiler, e.g. `gcc`, `clang`, etc. - On macOS, a working Xcode installation should provide this. - On Linux: @@ -20,36 +20,30 @@ To compile Pedalboard from scratch, the following packages will need to be insta ```shell git clone --recurse-submodules --shallow-submodules git@github.com:spotify/pedalboard.git cd pedalboard -pip3 install pybind11 tox +pip3 install pybind11 scikit-build-core tox pip3 install . ``` To compile a debug build of `pedalboard` that allows using a debugger (like gdb or lldb), use the following command to build the package locally and install a symbolic link for debugging: ```shell -python3 setup.py build develop +pip3 install --no-build-isolation -ve . ``` Then, you can `import pedalboard` from Python (or run the tests with `tox`) to test out your local changes. > If you're on macOS or Linux, you can try to compile a debug build _faster_ by using [Ccache](https://ccache.dev/): -> ## macOS +> ### macOS > ```shell > brew install ccache -> rm -rf build && CC="ccache clang" CXX="ccache clang++" DEBUG=1 python3 -j8 -m pip install -e . +> rm -rf build && CC="ccache clang" CXX="ccache clang++" DEBUG=1 pip3 install --no-build-isolation -ve . > ``` -> ## Linux -> e.g. +> ### Linux > ```shell > sudo yum install ccache # or apt, if on a Debian -> -> # If using GCC: -> rm -rf build && CC="ccache gcc" CXX="scripts/ccache_g++" DEBUG=1 python3 setup.py build -j8 develop -> -> # ...or if using Clang: -> rm -rf build && CC="ccache clang" CXX="scripts/ccache_clang++" DEBUG=1 python3 setup.py build -j8 develop +> rm -rf build && CC="ccache clang" CXX="ccache clang++" DEBUG=1 pip3 install --no-build-isolation -ve . > ``` -By default, [all `.cpp` and `.mm` files in the `pedalboard` directory (or subdirectories)](https://github.com/spotify/pedalboard/blob/master/setup.py#L129) will be automatically compiled by `setup.py`. +By default, all `.cpp` and `.mm` files in the `pedalboard` directory (and subdirectories) will be automatically compiled. While `pedalboard` is mostly C++ code, it ships with `.pyi` files to allow for type hints in text editors and via MyPy. To update the type hint files, use the following commands: diff --git a/pyproject.toml b/pyproject.toml index 950a0d596..58be2d0e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,7 @@ classifiers = [ "Programming Language :: Python :: 3.14", ] dependencies = ["numpy"] -version = "0.9.22" +dynamic = ["version"] [project.optional-dependencies] test = [ @@ -75,6 +75,9 @@ test = [ cmake.build-type = "Release" build.verbose = true logging.level = "DEBUG" +metadata.version.provider = "scikit_build_core.metadata.regex" +metadata.version.input = "pedalboard/version.py" +metadata.version.regex = '__version__\s*=\s*"(?P[^"]+)"' wheel.packages = ["pedalboard", "pedalboard.io", "pedalboard_native"] sdist.include = [ "pedalboard/**/*.py", diff --git a/scripts/ccache_clang++ b/scripts/ccache_clang++ deleted file mode 100755 index 52deb3cbf..000000000 --- a/scripts/ccache_clang++ +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash -# -# ccache_clang++ -# -# This script is a shim to allow the passing of the ccache command in to the -# CXX environmental variable on linux, which increases the speed of compilation - -$(ccache clang++ $@) diff --git a/scripts/ccache_g++ b/scripts/ccache_g++ deleted file mode 100755 index f3543a706..000000000 --- a/scripts/ccache_g++ +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash -# -# ccache_g++ -# -# This script is a shim to allow the passing of the ccache command in to the -# CXX environmental variable on linux, which increases the speed of compilation - -$(ccache g++ $@) diff --git a/setup.py b/setup.py deleted file mode 100644 index b5536e5d0..000000000 --- a/setup.py +++ /dev/null @@ -1,505 +0,0 @@ -#! /usr/bin/env python -# -# Copyright 2021 Spotify AB -# -# Licensed under the GNU Public License, Version 3.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.gnu.org/licenses/gpl-3.0.html -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -import os -import platform -from distutils.core import setup -from distutils.unixccompiler import UnixCCompiler -from pathlib import Path -from subprocess import check_output - -from pybind11.setup_helpers import Pybind11Extension, build_ext - -DEBUG = bool(int(os.environ.get("DEBUG", 0))) - -# C or C++ flags: -BASE_CPP_FLAGS = [ - "-Wall", -] -ALL_INCLUDES = [] -ALL_LINK_ARGS = [] -ALL_CFLAGS = [] -ALL_CPPFLAGS = [] -ALL_LIBRARIES = [] -ALL_SOURCE_PATHS = [] - -# Add JUCE-related flags: -ALL_CPPFLAGS.extend( - [ - "-DJUCE_DISPLAY_SPLASH_SCREEN=1", - "-DJUCE_USE_DARK_SPLASH_SCREEN=1", - "-DJUCE_MODULE_AVAILABLE_juce_audio_basics=1", - "-DJUCE_MODULE_AVAILABLE_juce_audio_formats=1", - "-DJUCE_MODULE_AVAILABLE_juce_audio_processors=1", - "-DJUCE_MODULE_AVAILABLE_juce_core=1", - "-DJUCE_MODULE_AVAILABLE_juce_data_structures=1", - "-DJUCE_MODULE_AVAILABLE_juce_dsp=1", - "-DJUCE_MODULE_AVAILABLE_juce_events=1", - "-DJUCE_MODULE_AVAILABLE_juce_graphics=1", - "-DJUCE_MODULE_AVAILABLE_juce_gui_basics=1", - "-DJUCE_MODULE_AVAILABLE_juce_gui_extra=1", - "-DJUCE_MODULE_AVAILABLE_juce_audio_devices=1", - "-DJUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1", - "-DJUCE_STRICT_REFCOUNTEDPOINTER=1", - "-DJUCE_STANDALONE_APPLICATION=1", - "-DJUCER_LINUX_MAKE_6D53C8B4=1", - "-DJUCE_APP_VERSION=1.0.0", - "-DJUCE_APP_VERSION_HEX=0x10000", - # Consoleapp flags: - "-DJucePlugin_Build_VST=0", - "-DJucePlugin_Build_VST3=0", - "-DJucePlugin_Build_AU=0", - "-DJucePlugin_Build_AUv3=0", - "-DJucePlugin_Build_RTAS=0", - "-DJucePlugin_Build_AAX=0", - "-DJucePlugin_Build_Standalone=0", - "-DJucePlugin_Build_Unity=0", - # "-DJUCE_PLUGINHOST_VST=1", # Include for VST2 support, not licensed by Steinberg - # "-DJUCE_PLUGINHOST_VST3=1", # Disable the built-in VST3 support, as we include our own. - # "-DJUCE_PLUGINHOST_LADSPA=1", # Include for LADSPA plugin support, Linux only. - "-DJUCE_DISABLE_JUCE_VERSION_PRINTING=1", - "-DJUCE_WEB_BROWSER=0", - "-DJUCE_USE_CURL=0", - "-DJUCE_USE_MP3AUDIOFORMAT=0", # We've patched this out too - "-DJUCE_USE_FLAC=0", # We've patched this out - # "-DJUCE_USE_FREETYPE=0", - "-DJUCE_MODAL_LOOPS_PERMITTED=1", - ] -) -ALL_INCLUDES.extend( - [ - "vendors/pybind11/include/", - "JUCE/modules/", - "JUCE/modules/juce_audio_processors/format_types/VST3_SDK/", - ] -) - -if "musllinux" in os.getenv("CIBW_BUILD", ""): - # For Alpine/musllinux compatibility: - ALL_CPPFLAGS.extend( - [ - "-D_NL_IDENTIFICATION_LANGUAGE=0x42", - "-D_NL_IDENTIFICATION_TERRITORY=0x43", - ] - ) - -# Rubber Band library: -ALL_CPPFLAGS.extend( - [ - "-DUSE_BQRESAMPLER=1", - "-D_HAS_STD_BYTE=0", - "-DNOMINMAX", - "-DALREADY_CONFIGURED", - ] -) - - -def ignore_files_matching(files, *matches): - matches = set(matches) - for match in matches: - new_files = [] - for file in files: - if match in str(file): - # print(f"Skipping compilation of: {file}") - pass - else: - new_files.append(file) - files = new_files - return files - - -# Platform-specific FFT speedup flags: -if platform.system() == "Windows" or "musllinux" in os.getenv("CIBW_BUILD", ""): - ALL_CPPFLAGS.append("-DUSE_BUILTIN_FFT") - ALL_CPPFLAGS.append("-DNO_THREADING") -elif platform.system() == "Darwin": - # No need for any threading code on MacOS; - # vDSP does all of this for us and these code paths are redundant. - ALL_CPPFLAGS.append("-DNO_THREADING") -elif platform.system() == "Linux": - # Use FFTW3 for FFTs on Linux, which should speed up Rubberband by 3-4x: - ALL_CPPFLAGS.extend( - [ - "-DHAVE_FFTW3=1", - "-DLACK_SINCOS=1", - "-DFFTW_DOUBLE_ONLY=1", - "-DUSE_PTHREADS", - ] - ) - ALL_INCLUDES += ["vendors/fftw3/api/", "vendors/fftw3/"] - fftw_paths = list(Path("vendors/fftw3/").glob("**/*.c")) - fftw_paths = ignore_files_matching( - fftw_paths, - # Don't bother compiling in Altivec or VSX (PowerPC) support; - # it's 2024, not 2004 (although RIP my G5 cheese grater) - "altivec", - "vsx", - # We're not using FFTW in multi-threaded mode: - "mpi", - "threads", - # No need for tests, tools, or support code: - "tests", - "tools", - "/support", - "common/", - "libbench", - # Ignore SSE, AVX2, AVX128, and AVX512 SIMD code; - # For Rubber Band's usage, just AVX gives us the - # largest speedup without bloating the binary - "sse2", - "avx2", - "avx512", - "kcvi", - "avx-128-fma", - "generic-simd", - ) - - # On ARM, ignore the X86-specific SIMD code: - if "arm" in platform.processor() or "aarch64" in platform.processor(): - fftw_paths = ignore_files_matching(fftw_paths, "avx", "/sse") - ALL_CFLAGS.append("-DHAVE_NEON=1") - else: - # And on x86, ignore the ARM-specific SIMD code (and KCVI; not GCC or Clang compatible). - fftw_paths = ignore_files_matching(fftw_paths, "neon") - # Use -march=native for local builds to optimize for the current CPU, - # but use a portable baseline for CI builds to avoid "Illegal instruction" errors - # when ccache restores objects built on different runner hardware. - if os.getenv("USE_PORTABLE_SIMD"): - ALL_CFLAGS.append("-mavx") - else: - ALL_CFLAGS.append("-march=native") - # Enable SIMD instructions: - ALL_CFLAGS.extend( - [ - # "-DHAVE_SSE2", - "-DHAVE_AVX", # Testing shows this is all we need! - # "-DHAVE_AVX_128_FMA", # AMD only - # "-DHAVE_AVX2", - # "-DHAVE_AVX512", # No measurable speed difference - # "-DHAVE_GENERIC_SIMD128", # Crashes! - # "-DHAVE_GENERIC_SIMD256", # Also crashes! - ] - ) - - ALL_SOURCE_PATHS += fftw_paths - - ALL_CFLAGS.extend( - [ - "-DHAVE_UINTPTR_T", - '-DPACKAGE="FFTW"', - '-DVERSION="0"', - '-DPACKAGE_VERSION="00000"', - '-DFFTW_CC="clang"', - "-includestring.h", - "-includestdint.h", - "-includevendors/fftw3/dft/codelet-dft.h", - "-includevendors/fftw3/rdft/codelet-rdft.h", - "-DHAVE_INTTYPES_H", - "-DHAVE_STDINT_H", - "-DHAVE_STDLIB_H", - "-DHAVE_STRING_H", - "-DHAVE_TIME_H", - "-DHAVE_UNISTD_H", - "-DHAVE_DECL_DRAND48", - "-DHAVE_DECL_SRAND48", - "-DHAVE_DECL_COSL", - "-DHAVE_DECL_SINL", - "-DHAVE_DECL_POSIX_MEMALIGN", - "-DHAVE_DRAND48", - "-DHAVE_SRAND48", - "-DHAVE_POSIX_MEMALIGN", - "-DHAVE_ISNAN", - "-DHAVE_SNPRINTF", - "-DHAVE_STRCHR", - "-DHAVE_SYSCTL", - "-DHAVE_GETTIMEOFDAY", - ] - ) - -ALL_SOURCE_PATHS += list(Path("vendors/rubberband/single").glob("*.cpp")) - -ALL_SOURCE_PATHS += list(Path("vendors").glob("*.c")) -ALL_INCLUDES += ["vendors/"] - -# LAME/mpglib: -LAME_FLAGS = ["-DHAVE_MPGLIB"] -LAME_CONFIG_FILE = str(Path("vendors/lame_config.h").resolve()) -if platform.system() == "Windows": - LAME_FLAGS.append(f"/FI{LAME_CONFIG_FILE}") - LAME_FLAGS.append("-DHAVE_XMMINTRIN_H") -else: - LAME_FLAGS.append(f"-include{LAME_CONFIG_FILE}") -ALL_CFLAGS.extend(LAME_FLAGS) -ALL_SOURCE_PATHS += list(Path("vendors/lame/libmp3lame").glob("*.c")) -ALL_SOURCE_PATHS += list(Path("vendors/lame/libmp3lame/vector").glob("*.c")) -ALL_SOURCE_PATHS += list(Path("vendors/lame/mpglib").glob("*.c")) -ALL_INCLUDES += [ - "vendors/lame/include/", - "vendors/lame/libmp3lame/", - "vendors/lame/", -] - -# libgsm -ALL_SOURCE_PATHS += [p for p in Path("vendors/libgsm/src").glob("*.c") if "toast" not in p.name] -ALL_INCLUDES += ["vendors/libgsm/inc"] - - -# Add platform-specific flags: -if platform.system() == "Darwin": - ALL_CPPFLAGS.append("-DMACOS=1") - ALL_CPPFLAGS.append("-DHAVE_VDSP=1") - if not DEBUG and not os.getenv("DISABLE_LTO"): - ALL_CPPFLAGS.append("-flto") - ALL_LINK_ARGS.append("-flto") - ALL_LINK_ARGS.append("-fvisibility=hidden") - ALL_CFLAGS += ["-Wno-comment"] -elif platform.system() == "Linux": - ALL_CPPFLAGS.append("-DLINUX=1") - # We use GCC on Linux, which doesn't take a value for the -flto flag: - if not DEBUG and not os.getenv("DISABLE_LTO"): - ALL_CPPFLAGS.append("-flto") - ALL_LINK_ARGS.append("-flto") - ALL_LINK_ARGS.append("-fvisibility=hidden") - ALL_CFLAGS += ["-Wno-comment"] -elif platform.system() == "Windows": - ALL_CPPFLAGS.append("-DWINDOWS=1") -else: - raise NotImplementedError( - "Not sure how to build JUCE on platform: {}!".format(platform.system()) - ) - - -if DEBUG: - ALL_CPPFLAGS += ["-DDEBUG=1", "-D_DEBUG=1"] - ALL_CPPFLAGS += ["-O0", "-g"] -else: - ALL_CPPFLAGS += ["/Ox" if platform.system() == "Windows" else "-O3"] - -if bool(int(os.environ.get("USE_ASAN", 0))): - ALL_CPPFLAGS += ["-fsanitize=address", "-fno-omit-frame-pointer"] - ALL_LINK_ARGS += ["-fsanitize=address"] - if platform.system() == "Linux": - ALL_LINK_ARGS += ["-shared-libasan", "-latomic"] -elif bool(int(os.environ.get("USE_TSAN", 0))): - ALL_CPPFLAGS += ["-fsanitize=thread"] - ALL_LINK_ARGS += ["-fsanitize=thread"] -elif bool(int(os.environ.get("USE_MSAN", 0))): - ALL_CPPFLAGS += ["-fsanitize=memory", "-fsanitize-memory-track-origins"] - ALL_LINK_ARGS += ["-fsanitize=memory"] - - -# Regardless of platform, allow our compiler to compile .mm files as Objective-C (required on MacOS) -UnixCCompiler.src_extensions.append(".mm") -UnixCCompiler.language_map[".mm"] = "objc++" - -# Add all Pedalboard C++ sources: -ALL_SOURCE_PATHS += list(Path("pedalboard").glob("**/*.cpp")) - -if platform.system() == "Darwin": - MACOS_FRAMEWORKS = [ - "Accelerate", - "AppKit", - "AudioToolbox", - "Cocoa", - "CoreAudio", - "CoreAudioKit", - "CoreMIDI", - "Foundation", - "IOKit", - "QuartzCore", - "WebKit", - ] - - # On MacOS, we link against some Objective-C system libraries, so we search - # for Objective-C++ files instead of C++ files. - for f in MACOS_FRAMEWORKS: - ALL_LINK_ARGS += ["-framework", f] - ALL_CPPFLAGS.append("-DJUCE_PLUGINHOST_AU=1") - ALL_CPPFLAGS.append("-xobjective-c++") - - # Replace .cpp sources with matching .mm sources on macOS to force the - # compiler to use Apple's Objective-C and Objective-C++ code. - for objc_source in Path("pedalboard").glob("**/*.mm"): - matching_cpp_source = next( - iter( - [ - cpp_source - for cpp_source in ALL_SOURCE_PATHS - if os.path.splitext(objc_source.name)[0] == os.path.splitext(cpp_source.name)[0] - ] - ), - None, - ) - if matching_cpp_source: - ALL_SOURCE_PATHS[ALL_SOURCE_PATHS.index(matching_cpp_source)] = objc_source - else: - ALL_SOURCE_PATHS.append(objc_source) - ALL_RESOLVED_SOURCE_PATHS = [str(p.resolve()) for p in ALL_SOURCE_PATHS] -elif platform.system() == "Linux": - for package in ["freetype2"]: - flags = ( - check_output(["pkg-config", "--cflags-only-I", package]) - .decode("utf-8") - .strip() - .split(" ") - ) - include_paths = [flag[2:] for flag in flags] - ALL_INCLUDES += include_paths - ALL_LINK_ARGS += ["-lfreetype"] - ALL_LINK_ARGS += ["-lasound"] - - ALL_RESOLVED_SOURCE_PATHS = [str(p.resolve()) for p in ALL_SOURCE_PATHS] -elif platform.system() == "Windows": - ALL_CPPFLAGS += ["-DJUCE_DLL_BUILD=1"] - # https://forum.juce.com/t/statically-linked-exe-in-win-10-not-working/25574/3 - ALL_LIBRARIES.extend( - [ - "kernel32", - "user32", - "gdi32", - "winspool", - "comdlg32", - "advapi32", - "shell32", - "ole32", - "oleaut32", - "uuid", - "odbc32", - "odbccp32", - ] - ) - ALL_RESOLVED_SOURCE_PATHS = [str(p.resolve()) for p in ALL_SOURCE_PATHS] -else: - raise NotImplementedError( - "Not sure how to build JUCE on platform: {}!".format(platform.system()) - ) - - -def patch_compile(original_compile): - """ - On GCC/Clang, we want to pass different arguments when compiling C files vs C++ files. - """ - - def new_compile(obj, src, ext, cc_args, extra_postargs, *args, **kwargs): - _cc_args = cc_args - - if ext in (".cpp", ".cxx", ".cc", ".mm"): - _cc_args = cc_args + ALL_CPPFLAGS - elif ext in (".c",): - # We're compiling C code, remove the -std= arg: - extra_postargs = [arg for arg in extra_postargs if "std=" not in arg] - _cc_args = cc_args + ALL_CFLAGS - - # Code in JUCE or vendors should not even know we're using Python: - should_omit_python_header = any(x in src for x in ("JUCE", "/juce_overrides/", "/vendors/")) - - # Remove the Python header from most files; we only need it when compiling - # This speeds up compile times on CI as most of the objects don't need Python - # headers at all, and including -I/include/python3.x/Python.h prevents us from - # reusing the same object file for different Python versions. - if any("include/python3" in arg for arg in _cc_args) and should_omit_python_header: - _cc_args = [arg for arg in _cc_args if "include/python3" not in arg] - - return original_compile(obj, src, ext, _cc_args, extra_postargs, *args, **kwargs) - - return new_compile - - -class BuildC_CxxExtensions(build_ext): - """ - Add custom logic for injecting different arguments when compiling C vs C++ files. - """ - - def initialize_options(self): - build_ext.initialize_options(self) - # If on CI, avoid breaking ccache by using a consistent - # output directory name regardless of Python version: - if os.getenv("CI"): - self.build_temp = "./build/temp" - - def build_extensions(self, *args, **kwargs): - self.compiler._compile = patch_compile(self.compiler._compile) - build_ext.build_extensions(self, *args, **kwargs) - - -if platform.system() == "Windows": - # The MSVCCompiler extension doesn't support per-file command line arguments, - # so let's merge all of the flags into one list here. - BASE_CPP_FLAGS.extend(ALL_CPPFLAGS) - BASE_CPP_FLAGS.extend(ALL_CFLAGS) - - -pedalboard_cpp = Pybind11Extension( - "pedalboard_native", - sources=ALL_RESOLVED_SOURCE_PATHS, - include_dirs=ALL_INCLUDES, - extra_compile_args=BASE_CPP_FLAGS, - extra_link_args=ALL_LINK_ARGS, - libraries=ALL_LIBRARIES, - language="c++", - cxx_std=17, - include_pybind11=False, -) - - -if DEBUG: - # Why does Pybind11 always remove debugging symbols? - pedalboard_cpp.extra_compile_args.remove("-g0") - -# read the contents of the README file -this_directory = Path(__file__).parent -long_description = (this_directory / "README.md").read_text() - -# read the contents of the version.py -version = {} -version_file_contents = (this_directory / "pedalboard" / "version.py").read_text() -exec(version_file_contents, version) - -logging.basicConfig(format="%(message)s") - -setup( - name="pedalboard", - version=version["__version__"], - author="Peter Sobot", - author_email="psobot@spotify.com", - description="A Python library for adding effects to audio.", - long_description=long_description, - long_description_content_type="text/markdown", - classifiers=[ - "Development Status :: 4 - Beta", - "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", - "Operating System :: MacOS", - "Operating System :: Microsoft :: Windows", - "Operating System :: POSIX :: Linux", - "Programming Language :: C++", - "Programming Language :: Python", - "Topic :: Multimedia :: Sound/Audio", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", - ], - ext_modules=[pedalboard_cpp], - install_requires=["numpy"], - packages=["pedalboard", "pedalboard.io", "pedalboard_native"], - package_data={ - "pedalboard": ["py.typed", "*.pyi", "**/*.pyi"], - "pedalboard_native": ["py.typed", "*.pyi", "**/*.pyi"], - }, - cmdclass={"build_ext": BuildC_CxxExtensions}, -)