setup.py (374 lines of code) (raw):
#! /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")
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",
]
)
if platform.system() == "Linux":
ALL_CFLAGS.append("-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
# re-using 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.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
],
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},
)