Repository: robbert-vdh/spectral-compressor
Branch: master
Commit: 6ea9563de6bb
Files: 16
Total size: 157.2 KB
Directory structure:
gitextract_bzp8s3kq/
├── .clang-format
├── .github/
│ └── workflows/
│ └── build.yml
├── .gitignore
├── CMakeLists.txt
├── COPYING
├── README.md
├── cmake/
│ └── CPM.cmake
└── src/
├── dsp/
│ ├── compressor.h
│ └── stft.h
├── editor.cpp
├── editor.h
├── processor.cpp
├── processor.h
├── ring.h
├── utils.cpp
└── utils.h
================================================
FILE CONTENTS
================================================
================================================
FILE: .clang-format
================================================
BasedOnStyle: Chromium
IndentWidth: 4
Standard: Cpp11
# Don't reflow nested comments
CommentPragmas: '^ *//'
================================================
FILE: .github/workflows/build.yml
================================================
name: Automated builds
on:
push:
branches:
- '**'
tags:
# Run when pushing version tags, since otherwise it's impossible to
# restart a successful build after pushing a tag
- '*.*.*'
pull_request:
branches:
- master
defaults:
run:
shell: bash
jobs:
build-bionic:
name: Build on Ubuntu 18.04
runs-on: ubuntu-18.04
outputs:
artifact-name: ${{ env.ARCHIVE_NAME }}
# GitHub actions does not allow you to share steps between jobs and their
# yaml parser does not support anchors, so we'll have to duplicate all of
# these steps
# https://github.community/t5/GitHub-Actions/reusing-sharing-inheriting-steps-between-jobs-declarations/td-p/37849
steps:
- uses: actions/checkout@v2
# Needed for git-describe to do anything useful
- name: Fetch all git history
run: git fetch --force --prune --tags --unshallow
- name: Determine build archive name
run: |
echo "ARCHIVE_NAME=spectral-compressor-$(git describe --always)-ubuntu-18.04-avx2" >> "$GITHUB_ENV"
# We should make a Docker container, but this works for now
- name: Install dependencies for JUCE
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxcursor-dev libxinerama-dev libxrandr-dev \
libasound2-dev
- name: Build the binaries
run: |
export CC=/usr/bin/gcc-10
export CXX=/usr/bin/g++-10
# Statically link to libstdc++ on Ubuntu 18.04 as we're compiling with
# a newer version of g++ than what's in the default repos
cmake -Bbuild -DCMAKE_BUILD_TYPE=Release -DFORCE_STATIC_LINKING=1
cmake --build build
# GitHub Actions doesn't let you create a tarball directly, but since we
# have no binaries a .zip archive should be fine
- name: Create an archive for the binaries
run: |
mkdir "$ARCHIVE_NAME"
# TODO: Add some point, also add a readme and a changelog to these archives
cp -r build/SpectralCompressor_artefacts/Release/VST3/Spectral\ Compressor.vst3 "$ARCHIVE_NAME"
- uses: actions/upload-artifact@v2
with:
name: ${{ env.ARCHIVE_NAME }}
path: ${{ env.ARCHIVE_NAME }}
build-windows:
name: Build on Windows
runs-on: windows-latest
outputs:
artifact-name: ${{ env.ARCHIVE_NAME }}
steps:
- uses: actions/checkout@v2
- name: Fetch all git history
run: git fetch --force --prune --tags --unshallow
- name: Determine build archive name
run: |
echo "ARCHIVE_NAME=spectral-compressor-$(git describe --always)-windows-avx2" >> "$GITHUB_ENV"
- name: Build the binaries
run: |
cmake -Bbuild -DCMAKE_BUILD_TYPE=Release -DFORCE_STATIC_LINKING=1
cmake --build build --config Release
- name: Create an archive for the binaries
run: |
mkdir "$ARCHIVE_NAME"
cp -r build/SpectralCompressor_artefacts/Release/VST3/Spectral\ Compressor.vst3 "$ARCHIVE_NAME"
- uses: actions/upload-artifact@v2
with:
name: ${{ env.ARCHIVE_NAME }}
path: ${{ env.ARCHIVE_NAME }}
build-macos:
name: Build on macOS
runs-on: macos-10.15
outputs:
artifact-name: ${{ env.ARCHIVE_NAME }}
steps:
- uses: actions/checkout@v2
- name: Fetch all git history
run: git fetch --force --prune --tags --unshallow
- name: Determine build archive name
run: |
echo "ARCHIVE_NAME=spectral-compressor-$(git describe --always)-macos-10.13-avx2" >> "$GITHUB_ENV"
- name: Build the binaries
run: |
cmake -Bbuild -DCMAKE_BUILD_TYPE=Release -DFORCE_STATIC_LINKING=1
cmake --build build --config Release
- name: Create an archive for the binaries
run: |
mkdir "$ARCHIVE_NAME"
cp -r build/SpectralCompressor_artefacts/Release/VST3/Spectral\ Compressor.vst3 "$ARCHIVE_NAME"
cp -r build/SpectralCompressor_artefacts/Release/AU/Spectral\ Compressor.component "$ARCHIVE_NAME"
- uses: actions/upload-artifact@v2
with:
name: ${{ env.ARCHIVE_NAME }}
path: ${{ env.ARCHIVE_NAME }}
build-macos-no-avx2:
name: Build on macOS without AVX2
runs-on: macos-10.15
outputs:
artifact-name: ${{ env.ARCHIVE_NAME }}
steps:
- uses: actions/checkout@v2
- name: Fetch all git history
run: git fetch --force --prune --tags --unshallow
- name: Determine build archive name
run: |
echo "ARCHIVE_NAME=spectral-compressor-$(git describe --always)-macos-10.13-avx" >> "$GITHUB_ENV"
- name: Build the binaries
run: |
cmake -Bbuild -DCMAKE_BUILD_TYPE=Release -DFORCE_STATIC_LINKING=1 -DWITH_FFTW_AVX2=OFF
cmake --build build --config Release
- name: Create an archive for the binaries
run: |
mkdir "$ARCHIVE_NAME"
cp -r build/SpectralCompressor_artefacts/Release/VST3/Spectral\ Compressor.vst3 "$ARCHIVE_NAME"
cp -r build/SpectralCompressor_artefacts/Release/AU/Spectral\ Compressor.component "$ARCHIVE_NAME"
- uses: actions/upload-artifact@v2
with:
name: ${{ env.ARCHIVE_NAME }}
path: ${{ env.ARCHIVE_NAME }}
================================================
FILE: .gitignore
================================================
build/
================================================
FILE: CMakeLists.txt
================================================
cmake_minimum_required(VERSION 3.15)
project(spectral_compressor VERSION 0.0.1 LANGUAGES CXX)
# TODO: Figure out a clean way to allow maintainers to disable all static
# linking and downloading
option(FORCE_STATIC_LINKING "Statically link all dependencies, for distribution" OFF)
# This option should not be necessary as FFTW should at runtime choose the
# correct version based on the current CPU, but apparently on MacOS it doesn't
# do that. This option is there specifically to support a single mid 2012
# MacBook Pro that does not yet support AVX2. It also only does anything unless
# `FORCE_STATIC_LINKING` is also enabled.
option(WITH_FFTW_AVX2 "Enable AVX2 support. By default both AVX and AVX2 are enabled." ON)
# CMake for some reason doesn't enable diagnostic colors by default
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
add_compile_options(-fdiagnostics-color=always)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
add_compile_options(-fcolor-diagnostics)
endif()
# Statically link the STL on Windows for the CI builds, and target a lower macOS version
if(FORCE_STATIC_LINKING)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13")
endif()
#
# Dependencies
#
# Fetch JUCE and other dependencies. For non-JUCE dependencies we'll check
# whether the package is available locally with pkgconfig before trying to
# download and build it ourselves.
include(cmake/CPM.cmake)
# FIXME: JUCE 6.1.3 and 6.1.4 will trigger an X11 error upon loading the plugin
# FIXME: Oh and JUCE 6.1.2 doesn't compile on macOS
if(APPLE)
CPMAddPackage("gh:juce-framework/JUCE#6.1.3")
else()
CPMAddPackage("gh:juce-framework/JUCE#6.1.2")
endif()
if(NOT FORCE_STATIC_LINKING)
find_package(PkgConfig)
if(PkgConfig_FOUND)
pkg_search_module(FFTW IMPORTED_TARGET fftw3f)
pkg_search_module(function2 IMPORTED_TARGET function2)
endif()
endif()
# Either dynamically link to the system FFTW, or statically link for
# distribution (FFTW also seems to rarely be shipped with static archives)
if(FFTW_FOUND AND NOT FORCE_STATIC_LINKING)
set(fftw_target PkgConfig::FFTW)
set(use_shared_fftw TRUE)
else()
message(STATUS "fftw3f not found using pkgconfig, falling back to a statically linked local build")
CPMAddPackage(
NAME FFTW
VERSIOIN 3.3.9
URL http://fftw.org/fftw-3.3.9.tar.gz
URL_HASH SHA256=bf2c7ce40b04ae811af714deb512510cc2c17b9ab9d6ddcf49fe4487eea7af3d
OPTIONS
"BUILD_SHARED_LIBS OFF"
"BUILD_TESTS OFF"
"ENABLE_FLOAT ON"
"ENABLE_AVX ON"
"ENABLE_AVX2 ${WITH_FFTW_AVX2}"
)
# CMake builds static libraries without -fPIC by default
set_target_properties(fftw3f PROPERTIES POSITION_INDEPENDENT_CODE ON)
set(fftw_target fftw3f)
set(use_static_fftw TRUE)
endif()
if(NOT function2_FOUND)
message(STATUS "function2 not found using pkgconfig, downloading from GitHub")
CPMAddPackage("gh:Naios/function2#4.1.0")
endif()
#
# Plugins
#
juce_add_plugin(SpectralCompressor
PRODUCT_NAME "Spectral Compressor"
COMPANY_NAME "Robbert van der Helm"
FORMATS VST3 AU
PLUGIN_MANUFACTURER_CODE RvdH
PLUGIN_CODE Spcc
IS_SYNTH FALSE
NEEDS_MIDI_INPUT FALSE
NEEDS_MIDI_OUTPUT FALSE
IS_MIDI_EFFECT FALSE
EDITOR_WANTS_KEYBOARD_FOCUS FALSE
VST3_CATEGORIES Fx Dynamics)
target_sources(SpectralCompressor PRIVATE
src/editor.cpp
src/processor.cpp
src/utils.cpp)
target_compile_definitions(SpectralCompressor PUBLIC
JUCE_WEB_BROWSER=0
JUCE_USE_CURL=0
JUCE_VST3_CAN_REPLACE_VST2=0
# We're licensed under the GPL
JUCE_DISPLAY_SPLASH_SCREEN=0
$<$<BOOL:${use_shared_fftw}>:JUCE_DSP_USE_SHARED_FFTW=1>
$<$<BOOL:${use_static_fftw}>:JUCE_DSP_USE_STATIC_FFTW=1>)
target_compile_features(SpectralCompressor PUBLIC cxx_std_20)
set_target_properties(SpectralCompressor PROPERTIES CXX_EXTENSIONS OFF)
# GCC 7+ no longer emits instructions for 128-bit compare-and-swaps and instead
# uses libatomic for this
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
target_link_libraries(SpectralCompressor PRIVATE -latomic)
endif()
# Statically link the STL on Linux for the CI builds
if(FORCE_STATIC_LINKING AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
target_link_libraries(SpectralCompressor PRIVATE -static-libstdc++)
endif()
target_link_libraries(SpectralCompressor
PUBLIC
juce::juce_recommended_warning_flags
juce::juce_recommended_lto_flags
juce::juce_recommended_config_flags
PRIVATE
juce::juce_audio_utils
juce::juce_dsp
${fftw_target}
function2)
================================================
FILE: COPYING
================================================
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
================================================
FILE: README.md
================================================
# Spectral Compressor
[](https://github.com/robbert-vdh/spectral-compressor/actions?query=workflow%3A%22Automated+builds%22+branch%3Amaster)
**Spectral Compressor has had a major rewrite with better DSP and more features
and is now part of [NIH-plug](https://github.com/robbert-vdh/nih-plug).** **This
version of the plugin will not be developed any more.**
Ever wondered what a 16384 band OTT would sound like? Neither did I.
## Download
You can download the latest development binaries for Linux, Windows and macOS
from the [automated
builds](https://github.com/robbert-vdh/spectral-compressor/actions?query=workflow%3A%22Automated+builds%22+branch%3Amaster)
page. GitHub requires you to be signed in to be able to download these files.
The macOS builds are compiled on macOS 10.15 Catalina and likely won't run on
anything before that. You may also have to [disable
Gatekeeper](https://disable-gatekeeper.github.io/) depending on your DAW as
Apple has recently made it more difficult to run unsigned code on macOS. I sadly
cannot provide any support for the Windows and macOS versions at the moment, but
the plugins should work!
## Building
To build the VST3 plugin, you'll need [CMake 3.15 or
higher](https://cliutils.gitlab.io/modern-cmake/chapters/intro/installing.html)
and a recent C++ compiler.
```shell
cmake -Bbuild -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel
```
You'll find the compiled plugin in `build/SpectralCompressor_artefacts/Release/VST3`.
### Static linking dependencies
By default this project will use the system's copy of FFTW (`fftw3f`) if it's
available, and it will fall back to building a static library and linking to
that. Adding `-DFORCE_STATIC_LINKING=ON` to the command line forces static
linking for distribution. This will also statically linking to the MSVC++
runtime on Windows.
================================================
FILE: cmake/CPM.cmake
================================================
# CPM.cmake - CMake's missing package manager
# ===========================================
# See https://github.com/cpm-cmake/CPM.cmake for usage and update instructions.
#
# MIT License
# -----------
#[[
Copyright (c) 2021 Lars Melchior and additional contributors
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.
]]
cmake_minimum_required(VERSION 3.14 FATAL_ERROR)
set(CURRENT_CPM_VERSION 0.31.1)
if(CPM_DIRECTORY)
if(NOT CPM_DIRECTORY STREQUAL CMAKE_CURRENT_LIST_DIR)
if(CPM_VERSION VERSION_LESS CURRENT_CPM_VERSION)
message(
AUTHOR_WARNING
"${CPM_INDENT} \
A dependency is using a more recent CPM version (${CURRENT_CPM_VERSION}) than the current project (${CPM_VERSION}). \
It is recommended to upgrade CPM to the most recent version. \
See https://github.com/cpm-cmake/CPM.cmake for more information."
)
endif()
if(${CMAKE_VERSION} VERSION_LESS "3.17.0")
include(FetchContent)
endif()
return()
endif()
get_property(
CPM_INITIALIZED GLOBAL ""
PROPERTY CPM_INITIALIZED
SET
)
if(CPM_INITIALIZED)
return()
endif()
endif()
set_property(GLOBAL PROPERTY CPM_INITIALIZED true)
option(CPM_USE_LOCAL_PACKAGES "Always try to use `find_package` to get dependencies"
$ENV{CPM_USE_LOCAL_PACKAGES}
)
option(CPM_LOCAL_PACKAGES_ONLY "Only use `find_package` to get dependencies"
$ENV{CPM_LOCAL_PACKAGES_ONLY}
)
option(CPM_DOWNLOAD_ALL "Always download dependencies from source" $ENV{CPM_DOWNLOAD_ALL})
option(CPM_DONT_UPDATE_MODULE_PATH "Don't update the module path to allow using find_package"
$ENV{CPM_DONT_UPDATE_MODULE_PATH}
)
option(CPM_DONT_CREATE_PACKAGE_LOCK "Don't create a package lock file in the binary path"
$ENV{CPM_DONT_CREATE_PACKAGE_LOCK}
)
option(CPM_INCLUDE_ALL_IN_PACKAGE_LOCK
"Add all packages added through CPM.cmake to the package lock"
$ENV{CPM_INCLUDE_ALL_IN_PACKAGE_LOCK}
)
set(CPM_VERSION
${CURRENT_CPM_VERSION}
CACHE INTERNAL ""
)
set(CPM_DIRECTORY
${CMAKE_CURRENT_LIST_DIR}
CACHE INTERNAL ""
)
set(CPM_FILE
${CMAKE_CURRENT_LIST_FILE}
CACHE INTERNAL ""
)
set(CPM_PACKAGES
""
CACHE INTERNAL ""
)
set(CPM_DRY_RUN
OFF
CACHE INTERNAL "Don't download or configure dependencies (for testing)"
)
if(DEFINED ENV{CPM_SOURCE_CACHE})
set(CPM_SOURCE_CACHE_DEFAULT $ENV{CPM_SOURCE_CACHE})
else()
set(CPM_SOURCE_CACHE_DEFAULT OFF)
endif()
set(CPM_SOURCE_CACHE
${CPM_SOURCE_CACHE_DEFAULT}
CACHE PATH "Directory to download CPM dependencies"
)
if(NOT CPM_DONT_UPDATE_MODULE_PATH)
set(CPM_MODULE_PATH
"${CMAKE_BINARY_DIR}/CPM_modules"
CACHE INTERNAL ""
)
# remove old modules
file(REMOVE_RECURSE ${CPM_MODULE_PATH})
file(MAKE_DIRECTORY ${CPM_MODULE_PATH})
# locally added CPM modules should override global packages
set(CMAKE_MODULE_PATH "${CPM_MODULE_PATH};${CMAKE_MODULE_PATH}")
endif()
if(NOT CPM_DONT_CREATE_PACKAGE_LOCK)
set(CPM_PACKAGE_LOCK_FILE
"${CMAKE_BINARY_DIR}/cpm-package-lock.cmake"
CACHE INTERNAL ""
)
file(WRITE ${CPM_PACKAGE_LOCK_FILE}
"# CPM Package Lock\n# This file should be committed to version control\n\n"
)
endif()
include(FetchContent)
include(CMakeParseArguments)
# Try to infer package name from git repository uri (path or url)
function(cpm_package_name_from_git_uri URI RESULT)
if("${URI}" MATCHES "([^/:]+)/?.git/?$")
set(${RESULT}
${CMAKE_MATCH_1}
PARENT_SCOPE
)
else()
unset(${RESULT} PARENT_SCOPE)
endif()
endfunction()
# Try to infer package name and version from a url
function(cpm_package_name_and_ver_from_url url outName outVer)
if(url MATCHES "[/\\?]([a-zA-Z0-9_\\.-]+)\\.(tar|tar\\.gz|tar\\.bz2|zip|ZIP)(\\?|/|$)")
# We matched an archive
set(filename "${CMAKE_MATCH_1}")
if(filename MATCHES "([a-zA-Z0-9_\\.-]+)[_-]v?(([0-9]+\\.)*[0-9]+[a-zA-Z0-9]*)")
# We matched <name>-<version> (ie foo-1.2.3)
set(${outName}
"${CMAKE_MATCH_1}"
PARENT_SCOPE
)
set(${outVer}
"${CMAKE_MATCH_2}"
PARENT_SCOPE
)
elseif(filename MATCHES "(([0-9]+\\.)+[0-9]+[a-zA-Z0-9]*)")
# We couldn't find a name, but we found a version
#
# In many cases (which we don't handle here) the url would look something like
# `irrelevant/ACTUAL_PACKAGE_NAME/irrelevant/1.2.3.zip`. In such a case we can't possibly
# distinguish the package name from the irrelevant bits. Moreover if we try to match the
# package name from the filename, we'd get bogus at best.
unset(${outName} PARENT_SCOPE)
set(${outVer}
"${CMAKE_MATCH_1}"
PARENT_SCOPE
)
else()
# Boldly assume that the file name is the package name.
#
# Yes, something like `irrelevant/ACTUAL_NAME/irrelevant/download.zip` will ruin our day, but
# such cases should be quite rare. No popular service does this... we think.
set(${outName}
"${filename}"
PARENT_SCOPE
)
unset(${outVer} PARENT_SCOPE)
endif()
else()
# No ideas yet what to do with non-archives
unset(${outName} PARENT_SCOPE)
unset(${outVer} PARENT_SCOPE)
endif()
endfunction()
# Initialize logging prefix
if(NOT CPM_INDENT)
set(CPM_INDENT
"CPM:"
CACHE INTERNAL ""
)
endif()
function(cpm_find_package NAME VERSION)
string(REPLACE " " ";" EXTRA_ARGS "${ARGN}")
find_package(${NAME} ${VERSION} ${EXTRA_ARGS} QUIET)
if(${CPM_ARGS_NAME}_FOUND)
message(STATUS "${CPM_INDENT} using local package ${CPM_ARGS_NAME}@${VERSION}")
CPMRegisterPackage(${CPM_ARGS_NAME} "${VERSION}")
set(CPM_PACKAGE_FOUND
YES
PARENT_SCOPE
)
else()
set(CPM_PACKAGE_FOUND
NO
PARENT_SCOPE
)
endif()
endfunction()
# Create a custom FindXXX.cmake module for a CPM package This prevents `find_package(NAME)` from
# finding the system library
function(cpm_create_module_file Name)
if(NOT CPM_DONT_UPDATE_MODULE_PATH)
# erase any previous modules
file(WRITE ${CPM_MODULE_PATH}/Find${Name}.cmake
"include(${CPM_FILE})\n${ARGN}\nset(${Name}_FOUND TRUE)"
)
endif()
endfunction()
# Find a package locally or fallback to CPMAddPackage
function(CPMFindPackage)
set(oneValueArgs NAME VERSION GIT_TAG FIND_PACKAGE_ARGUMENTS)
cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "" ${ARGN})
if(NOT DEFINED CPM_ARGS_VERSION)
if(DEFINED CPM_ARGS_GIT_TAG)
cpm_get_version_from_git_tag("${CPM_ARGS_GIT_TAG}" CPM_ARGS_VERSION)
endif()
endif()
if(CPM_DOWNLOAD_ALL)
CPMAddPackage(${ARGN})
cpm_export_variables(${CPM_ARGS_NAME})
return()
endif()
cpm_check_if_package_already_added(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}" "${CPM_ARGS_OPTIONS}")
if(CPM_PACKAGE_ALREADY_ADDED)
cpm_export_variables(${CPM_ARGS_NAME})
return()
endif()
cpm_find_package(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}" ${CPM_ARGS_FIND_PACKAGE_ARGUMENTS})
if(NOT CPM_PACKAGE_FOUND)
CPMAddPackage(${ARGN})
cpm_export_variables(${CPM_ARGS_NAME})
endif()
endfunction()
# checks if a package has been added before
function(cpm_check_if_package_already_added CPM_ARGS_NAME CPM_ARGS_VERSION CPM_ARGS_OPTIONS)
if("${CPM_ARGS_NAME}" IN_LIST CPM_PACKAGES)
CPMGetPackageVersion(${CPM_ARGS_NAME} CPM_PACKAGE_VERSION)
if("${CPM_PACKAGE_VERSION}" VERSION_LESS "${CPM_ARGS_VERSION}")
message(
WARNING
"${CPM_INDENT} requires a newer version of ${CPM_ARGS_NAME} (${CPM_ARGS_VERSION}) than currently included (${CPM_PACKAGE_VERSION})."
)
endif()
if(CPM_ARGS_OPTIONS)
foreach(OPTION ${CPM_ARGS_OPTIONS})
cpm_parse_option(${OPTION})
if(NOT "${${OPTION_KEY}}" STREQUAL "${OPTION_VALUE}")
message(
WARNING
"${CPM_INDENT} ignoring package option for ${CPM_ARGS_NAME}: ${OPTION_KEY} = ${OPTION_VALUE} (${${OPTION_KEY}})"
)
endif()
endforeach()
endif()
cpm_get_fetch_properties(${CPM_ARGS_NAME})
set(${CPM_ARGS_NAME}_ADDED NO)
set(CPM_PACKAGE_ALREADY_ADDED
YES
PARENT_SCOPE
)
cpm_export_variables(${CPM_ARGS_NAME})
else()
set(CPM_PACKAGE_ALREADY_ADDED
NO
PARENT_SCOPE
)
endif()
endfunction()
# Parse the argument of CPMAddPackage in case a single one was provided and convert it to a list of
# arguments which can then be parsed idiomatically. For example gh:foo/bar@1.2.3 will be converted
# to: GITHUB_REPOSITORY;foo/bar;VERSION;1.2.3
function(cpm_parse_add_package_single_arg arg outArgs)
# Look for a scheme
if("${arg}" MATCHES "^([a-zA-Z]+):(.+)$")
string(TOLOWER "${CMAKE_MATCH_1}" scheme)
set(uri "${CMAKE_MATCH_2}")
# Check for CPM-specific schemes
if(scheme STREQUAL "gh")
set(out "GITHUB_REPOSITORY;${uri}")
set(packageType "git")
elseif(scheme STREQUAL "gl")
set(out "GITLAB_REPOSITORY;${uri}")
set(packageType "git")
# A CPM-specific scheme was not found. Looks like this is a generic URL so try to determine
# type
elseif(arg MATCHES ".git/?(@|#|$)")
set(out "GIT_REPOSITORY;${arg}")
set(packageType "git")
else()
# Fall back to a URL
set(out "URL;${arg}")
set(packageType "archive")
# We could also check for SVN since FetchContent supports it, but SVN is so rare these days.
# We just won't bother with the additional complexity it will induce in this function. SVN is
# done by multi-arg
endif()
else()
if(arg MATCHES ".git/?(@|#|$)")
set(out "GIT_REPOSITORY;${arg}")
set(packageType "git")
else()
# Give up
message(FATAL_ERROR "CPM: Can't determine package type of '${arg}'")
endif()
endif()
# For all packages we interpret @... as version. Only replace the last occurence. Thus URIs
# containing '@' can be used
string(REGEX REPLACE "@([^@]+)$" ";VERSION;\\1" out "${out}")
# Parse the rest according to package type
if(packageType STREQUAL "git")
# For git repos we interpret #... as a tag or branch or commit hash
string(REGEX REPLACE "#([^#]+)$" ";GIT_TAG;\\1" out "${out}")
elseif(packageType STREQUAL "archive")
# For archives we interpret #... as a URL hash.
string(REGEX REPLACE "#([^#]+)$" ";URL_HASH;\\1" out "${out}")
# We don't try to parse the version if it's not provided explicitly. cpm_get_version_from_url
# should do this at a later point
else()
# We should never get here. This is an assertion and hitting it means there's a bug in the code
# above. A packageType was set, but not handled by this if-else.
message(FATAL_ERROR "CPM: Unsupported package type '${packageType}' of '${arg}'")
endif()
set(${outArgs}
${out}
PARENT_SCOPE
)
endfunction()
# Download and add a package from source
function(CPMAddPackage)
list(LENGTH ARGN argnLength)
if(argnLength EQUAL 1)
cpm_parse_add_package_single_arg("${ARGN}" ARGN)
# The shorthand syntax implies EXCLUDE_FROM_ALL
set(ARGN "${ARGN};EXCLUDE_FROM_ALL;YES")
endif()
set(oneValueArgs
NAME
FORCE
VERSION
GIT_TAG
DOWNLOAD_ONLY
GITHUB_REPOSITORY
GITLAB_REPOSITORY
GIT_REPOSITORY
SOURCE_DIR
DOWNLOAD_COMMAND
FIND_PACKAGE_ARGUMENTS
NO_CACHE
GIT_SHALLOW
EXCLUDE_FROM_ALL
)
set(multiValueArgs URL OPTIONS)
cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "${multiValueArgs}" "${ARGN}")
# Set default values for arguments
if(NOT DEFINED CPM_ARGS_VERSION)
if(DEFINED CPM_ARGS_GIT_TAG)
cpm_get_version_from_git_tag("${CPM_ARGS_GIT_TAG}" CPM_ARGS_VERSION)
endif()
endif()
if(CPM_ARGS_DOWNLOAD_ONLY)
set(DOWNLOAD_ONLY ${CPM_ARGS_DOWNLOAD_ONLY})
else()
set(DOWNLOAD_ONLY NO)
endif()
if(DEFINED CPM_ARGS_GITHUB_REPOSITORY)
set(CPM_ARGS_GIT_REPOSITORY "https://github.com/${CPM_ARGS_GITHUB_REPOSITORY}.git")
endif()
if(DEFINED CPM_ARGS_GITLAB_REPOSITORY)
set(CPM_ARGS_GIT_REPOSITORY "https://gitlab.com/${CPM_ARGS_GITLAB_REPOSITORY}.git")
endif()
if(DEFINED CPM_ARGS_GIT_REPOSITORY)
list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_REPOSITORY ${CPM_ARGS_GIT_REPOSITORY})
if(NOT DEFINED CPM_ARGS_GIT_TAG)
set(CPM_ARGS_GIT_TAG v${CPM_ARGS_VERSION})
endif()
# If a name wasn't provided, try to infer it from the git repo
if(NOT DEFINED CPM_ARGS_NAME)
cpm_package_name_from_git_uri(${CPM_ARGS_GIT_REPOSITORY} CPM_ARGS_NAME)
endif()
endif()
set(CPM_SKIP_FETCH FALSE)
if(DEFINED CPM_ARGS_GIT_TAG)
list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_TAG ${CPM_ARGS_GIT_TAG})
# If GIT_SHALLOW is explicitly specified, honor the value.
if(DEFINED CPM_ARGS_GIT_SHALLOW)
list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_SHALLOW ${CPM_ARGS_GIT_SHALLOW})
endif()
endif()
if(DEFINED CPM_ARGS_URL)
# If a name or version aren't provided, try to infer them from the URL
list(GET CPM_ARGS_URL 0 firstUrl)
cpm_package_name_and_ver_from_url(${firstUrl} nameFromUrl verFromUrl)
# If we fail to obtain name and version from the first URL, we could try other URLs if any.
# However multiple URLs are expected to be quite rare, so for now we won't bother.
# If the caller provided their own name and version, they trump the inferred ones.
if(NOT DEFINED CPM_ARGS_NAME)
set(CPM_ARGS_NAME ${nameFromUrl})
endif()
if(NOT DEFINED CPM_ARGS_VERSION)
set(CPM_ARGS_VERSION ${verFromUrl})
endif()
list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS URL "${CPM_ARGS_URL}")
endif()
# Check for required arguments
if(NOT DEFINED CPM_ARGS_NAME)
message(
FATAL_ERROR
"CPM: 'NAME' was not provided and couldn't be automatically inferred for package added with arguments: '${ARGN}'"
)
endif()
# Check if package has been added before
cpm_check_if_package_already_added(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}" "${CPM_ARGS_OPTIONS}")
if(CPM_PACKAGE_ALREADY_ADDED)
cpm_export_variables(${CPM_ARGS_NAME})
return()
endif()
# Check for manual overrides
if(NOT CPM_ARGS_FORCE AND NOT "${CPM_${CPM_ARGS_NAME}_SOURCE}" STREQUAL "")
set(PACKAGE_SOURCE ${CPM_${CPM_ARGS_NAME}_SOURCE})
set(CPM_${CPM_ARGS_NAME}_SOURCE "")
CPMAddPackage(
NAME ${CPM_ARGS_NAME}
SOURCE_DIR ${PACKAGE_SOURCE}
FORCE True
OPTIONS ${CPM_ARGS_OPTIONS}
)
cpm_export_variables(${CPM_ARGS_NAME})
return()
endif()
# Check for available declaration
if(NOT CPM_ARGS_FORCE AND NOT "${CPM_DECLARATION_${CPM_ARGS_NAME}}" STREQUAL "")
set(declaration ${CPM_DECLARATION_${CPM_ARGS_NAME}})
set(CPM_DECLARATION_${CPM_ARGS_NAME} "")
CPMAddPackage(${declaration})
cpm_export_variables(${CPM_ARGS_NAME})
# checking again to ensure version and option compatibility
cpm_check_if_package_already_added(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}" "${CPM_ARGS_OPTIONS}")
return()
endif()
if(CPM_USE_LOCAL_PACKAGES OR CPM_LOCAL_PACKAGES_ONLY)
cpm_find_package(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}" ${CPM_ARGS_FIND_PACKAGE_ARGUMENTS})
if(CPM_PACKAGE_FOUND)
cpm_export_variables(${CPM_ARGS_NAME})
return()
endif()
if(CPM_LOCAL_PACKAGES_ONLY)
message(
SEND_ERROR
"CPM: ${CPM_ARGS_NAME} not found via find_package(${CPM_ARGS_NAME} ${CPM_ARGS_VERSION})"
)
endif()
endif()
CPMRegisterPackage("${CPM_ARGS_NAME}" "${CPM_ARGS_VERSION}")
if(CPM_ARGS_OPTIONS)
foreach(OPTION ${CPM_ARGS_OPTIONS})
cpm_parse_option(${OPTION})
set(${OPTION_KEY}
${OPTION_VALUE}
CACHE INTERNAL ""
)
endforeach()
endif()
if(DEFINED CPM_ARGS_GIT_TAG)
set(PACKAGE_INFO "${CPM_ARGS_GIT_TAG}")
elseif(DEFINED CPM_ARGS_SOURCE_DIR)
set(PACKAGE_INFO "${CPM_ARGS_SOURCE_DIR}")
else()
set(PACKAGE_INFO "${CPM_ARGS_VERSION}")
endif()
if(DEFINED CPM_ARGS_DOWNLOAD_COMMAND)
list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS DOWNLOAD_COMMAND ${CPM_ARGS_DOWNLOAD_COMMAND})
elseif(DEFINED CPM_ARGS_SOURCE_DIR)
list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS SOURCE_DIR ${CPM_ARGS_SOURCE_DIR})
elseif(CPM_SOURCE_CACHE AND NOT CPM_ARGS_NO_CACHE)
string(TOLOWER ${CPM_ARGS_NAME} lower_case_name)
set(origin_parameters ${CPM_ARGS_UNPARSED_ARGUMENTS})
list(SORT origin_parameters)
string(SHA1 origin_hash "${origin_parameters}")
set(download_directory ${CPM_SOURCE_CACHE}/${lower_case_name}/${origin_hash})
list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS SOURCE_DIR ${download_directory})
if(EXISTS ${download_directory})
# avoid FetchContent modules to improve performance
set(${CPM_ARGS_NAME}_BINARY_DIR ${CMAKE_BINARY_DIR}/_deps/${lower_case_name}-build)
set(${CPM_ARGS_NAME}_ADDED YES)
set(${CPM_ARGS_NAME}_SOURCE_DIR ${download_directory})
if(NOT CPM_ARGS_DOWNLOAD_ONLY AND EXISTS ${download_directory}/CMakeLists.txt)
cpm_add_subdirectory(
${download_directory} ${${CPM_ARGS_NAME}_BINARY_DIR} "${CPM_ARGS_EXCLUDE_FROM_ALL}"
)
endif()
set(CPM_SKIP_FETCH TRUE)
set(PACKAGE_INFO "${PACKAGE_INFO} at ${download_directory}")
else()
# Enable shallow clone when GIT_TAG is not a commit hash. Our guess may not be accurate, but
# it should guarantee no commit hash get mis-detected.
if(NOT DEFINED CPM_ARGS_GIT_SHALLOW)
cpm_is_git_tag_commit_hash("${CPM_ARGS_GIT_TAG}" IS_HASH)
if(NOT ${IS_HASH})
list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_SHALLOW TRUE)
endif()
endif()
# remove timestamps so CMake will re-download the dependency
file(REMOVE_RECURSE ${CMAKE_BINARY_DIR}/_deps/${lower_case_name}-subbuild)
set(PACKAGE_INFO "${PACKAGE_INFO} to ${download_directory}")
endif()
endif()
cpm_create_module_file(${CPM_ARGS_NAME} "CPMAddPackage(${ARGN})")
if(CPM_PACKAGE_LOCK_ENABLED)
if((CPM_ARGS_VERSION AND NOT CPM_ARGS_SOURCE_DIR) OR CPM_INCLUDE_ALL_IN_PACKAGE_LOCK)
cpm_add_to_package_lock(${CPM_ARGS_NAME} "${ARGN}")
elseif(CPM_ARGS_SOURCE_DIR)
cpm_add_comment_to_package_lock(${CPM_ARGS_NAME} "local directory")
else()
cpm_add_comment_to_package_lock(${CPM_ARGS_NAME} "${ARGN}")
endif()
endif()
message(
STATUS "${CPM_INDENT} adding package ${CPM_ARGS_NAME}@${CPM_ARGS_VERSION} (${PACKAGE_INFO})"
)
if(NOT CPM_SKIP_FETCH)
cpm_declare_fetch(
"${CPM_ARGS_NAME}" "${CPM_ARGS_VERSION}" "${PACKAGE_INFO}" "${CPM_ARGS_UNPARSED_ARGUMENTS}"
)
cpm_fetch_package("${CPM_ARGS_NAME}" "${DOWNLOAD_ONLY}" "${CPM_ARGS_EXCLUDE_FROM_ALL}")
cpm_get_fetch_properties("${CPM_ARGS_NAME}")
endif()
set(${CPM_ARGS_NAME}_ADDED YES)
cpm_export_variables("${CPM_ARGS_NAME}")
endfunction()
# Fetch a previously declared package
macro(CPMGetPackage Name)
if(DEFINED "CPM_DECLARATION_${Name}")
CPMAddPackage(NAME ${Name})
else()
message(SEND_ERROR "Cannot retrieve package ${Name}: no declaration available")
endif()
endmacro()
# export variables available to the caller to the parent scope expects ${CPM_ARGS_NAME} to be set
macro(cpm_export_variables name)
set(${name}_SOURCE_DIR
"${${name}_SOURCE_DIR}"
PARENT_SCOPE
)
set(${name}_BINARY_DIR
"${${name}_BINARY_DIR}"
PARENT_SCOPE
)
set(${name}_ADDED
"${${name}_ADDED}"
PARENT_SCOPE
)
endmacro()
# declares a package, so that any call to CPMAddPackage for the package name will use these
# arguments instead. Previous declarations will not be overriden.
macro(CPMDeclarePackage Name)
if(NOT DEFINED "CPM_DECLARATION_${Name}")
set("CPM_DECLARATION_${Name}" "${ARGN}")
endif()
endmacro()
function(cpm_add_to_package_lock Name)
if(NOT CPM_DONT_CREATE_PACKAGE_LOCK)
cpm_prettify_package_arguments(PRETTY_ARGN false ${ARGN})
file(APPEND ${CPM_PACKAGE_LOCK_FILE} "# ${Name}\nCPMDeclarePackage(${Name}\n${PRETTY_ARGN})\n")
endif()
endfunction()
function(cpm_add_comment_to_package_lock Name)
if(NOT CPM_DONT_CREATE_PACKAGE_LOCK)
cpm_prettify_package_arguments(PRETTY_ARGN true ${ARGN})
file(APPEND ${CPM_PACKAGE_LOCK_FILE}
"# ${Name} (unversioned)\n# CPMDeclarePackage(${Name}\n${PRETTY_ARGN}#)\n"
)
endif()
endfunction()
# includes the package lock file if it exists and creates a target `cpm-write-package-lock` to
# update it
macro(CPMUsePackageLock file)
if(NOT CPM_DONT_CREATE_PACKAGE_LOCK)
get_filename_component(CPM_ABSOLUTE_PACKAGE_LOCK_PATH ${file} ABSOLUTE)
if(EXISTS ${CPM_ABSOLUTE_PACKAGE_LOCK_PATH})
include(${CPM_ABSOLUTE_PACKAGE_LOCK_PATH})
endif()
if(NOT TARGET cpm-update-package-lock)
add_custom_target(
cpm-update-package-lock COMMAND ${CMAKE_COMMAND} -E copy ${CPM_PACKAGE_LOCK_FILE}
${CPM_ABSOLUTE_PACKAGE_LOCK_PATH}
)
endif()
set(CPM_PACKAGE_LOCK_ENABLED true)
endif()
endmacro()
# registers a package that has been added to CPM
function(CPMRegisterPackage PACKAGE VERSION)
list(APPEND CPM_PACKAGES ${PACKAGE})
set(CPM_PACKAGES
${CPM_PACKAGES}
CACHE INTERNAL ""
)
set("CPM_PACKAGE_${PACKAGE}_VERSION"
${VERSION}
CACHE INTERNAL ""
)
endfunction()
# retrieve the current version of the package to ${OUTPUT}
function(CPMGetPackageVersion PACKAGE OUTPUT)
set(${OUTPUT}
"${CPM_PACKAGE_${PACKAGE}_VERSION}"
PARENT_SCOPE
)
endfunction()
# declares a package in FetchContent_Declare
function(cpm_declare_fetch PACKAGE VERSION INFO)
if(${CPM_DRY_RUN})
message(STATUS "${CPM_INDENT} package not declared (dry run)")
return()
endif()
FetchContent_Declare(${PACKAGE} ${ARGN})
endfunction()
# returns properties for a package previously defined by cpm_declare_fetch
function(cpm_get_fetch_properties PACKAGE)
if(${CPM_DRY_RUN})
return()
endif()
FetchContent_GetProperties(${PACKAGE})
string(TOLOWER ${PACKAGE} lpackage)
set(${PACKAGE}_SOURCE_DIR
"${${lpackage}_SOURCE_DIR}"
PARENT_SCOPE
)
set(${PACKAGE}_BINARY_DIR
"${${lpackage}_BINARY_DIR}"
PARENT_SCOPE
)
endfunction()
function(cpm_add_subdirectory SOURCE_DIR BINARY_DIR EXCLUDE)
if(EXCLUDE)
set(addSubdirectoryExtraArgs EXCLUDE_FROM_ALL)
else()
set(addSubdirectoryExtraArgs "")
endif()
add_subdirectory(${SOURCE_DIR} ${BINARY_DIR} ${addSubdirectoryExtraArgs})
endfunction()
# downloads a previously declared package via FetchContent
function(cpm_fetch_package PACKAGE DOWNLOAD_ONLY EXCLUDE)
if(${CPM_DRY_RUN})
message(STATUS "${CPM_INDENT} package ${PACKAGE} not fetched (dry run)")
return()
endif()
FetchContent_GetProperties(${PACKAGE})
string(TOLOWER "${PACKAGE}" lower_case_name)
if(NOT ${lower_case_name}_POPULATED)
FetchContent_Populate(${PACKAGE})
if(NOT DOWNLOAD_ONLY AND EXISTS ${${lower_case_name}_SOURCE_DIR}/CMakeLists.txt)
set(CPM_OLD_INDENT "${CPM_INDENT}")
set(CPM_INDENT "${CPM_INDENT} ${PACKAGE}:")
cpm_add_subdirectory(
${${lower_case_name}_SOURCE_DIR} ${${lower_case_name}_BINARY_DIR} "${EXCLUDE}"
)
set(CPM_INDENT "${CPM_OLD_INDENT}")
endif()
endif()
endfunction()
# splits a package option
function(cpm_parse_option OPTION)
string(REGEX MATCH "^[^ ]+" OPTION_KEY ${OPTION})
string(LENGTH ${OPTION} OPTION_LENGTH)
string(LENGTH ${OPTION_KEY} OPTION_KEY_LENGTH)
if(OPTION_KEY_LENGTH STREQUAL OPTION_LENGTH)
# no value for key provided, assume user wants to set option to "ON"
set(OPTION_VALUE "ON")
else()
math(EXPR OPTION_KEY_LENGTH "${OPTION_KEY_LENGTH}+1")
string(SUBSTRING ${OPTION} "${OPTION_KEY_LENGTH}" "-1" OPTION_VALUE)
endif()
set(OPTION_KEY
"${OPTION_KEY}"
PARENT_SCOPE
)
set(OPTION_VALUE
"${OPTION_VALUE}"
PARENT_SCOPE
)
endfunction()
# guesses the package version from a git tag
function(cpm_get_version_from_git_tag GIT_TAG RESULT)
string(LENGTH ${GIT_TAG} length)
if(length EQUAL 40)
# GIT_TAG is probably a git hash
set(${RESULT}
0
PARENT_SCOPE
)
else()
string(REGEX MATCH "v?([0123456789.]*).*" _ ${GIT_TAG})
set(${RESULT}
${CMAKE_MATCH_1}
PARENT_SCOPE
)
endif()
endfunction()
# guesses if the git tag is a commit hash or an actual tag or a branch nane.
function(cpm_is_git_tag_commit_hash GIT_TAG RESULT)
string(LENGTH "${GIT_TAG}" length)
# full hash has 40 characters, and short hash has at least 7 characters.
if(length LESS 7 OR length GREATER 40)
set(${RESULT}
0
PARENT_SCOPE
)
else()
if(${GIT_TAG} MATCHES "^[a-fA-F0-9]+$")
set(${RESULT}
1
PARENT_SCOPE
)
else()
set(${RESULT}
0
PARENT_SCOPE
)
endif()
endif()
endfunction()
function(cpm_prettify_package_arguments OUT_VAR IS_IN_COMMENT)
set(oneValueArgs
NAME
FORCE
VERSION
GIT_TAG
DOWNLOAD_ONLY
GITHUB_REPOSITORY
GITLAB_REPOSITORY
GIT_REPOSITORY
SOURCE_DIR
DOWNLOAD_COMMAND
FIND_PACKAGE_ARGUMENTS
NO_CACHE
GIT_SHALLOW
)
set(multiValueArgs OPTIONS)
cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
foreach(oneArgName ${oneValueArgs})
if(DEFINED CPM_ARGS_${oneArgName})
if(${IS_IN_COMMENT})
string(APPEND PRETTY_OUT_VAR "#")
endif()
if(${oneArgName} STREQUAL "SOURCE_DIR")
string(REPLACE ${CMAKE_SOURCE_DIR} "\${CMAKE_SOURCE_DIR}" CPM_ARGS_${oneArgName}
${CPM_ARGS_${oneArgName}}
)
endif()
string(APPEND PRETTY_OUT_VAR " ${oneArgName} ${CPM_ARGS_${oneArgName}}\n")
endif()
endforeach()
foreach(multiArgName ${multiValueArgs})
if(DEFINED CPM_ARGS_${multiArgName})
if(${IS_IN_COMMENT})
string(APPEND PRETTY_OUT_VAR "#")
endif()
string(APPEND PRETTY_OUT_VAR " ${multiArgName}\n")
foreach(singleOption ${CPM_ARGS_${multiArgName}})
if(${IS_IN_COMMENT})
string(APPEND PRETTY_OUT_VAR "#")
endif()
string(APPEND PRETTY_OUT_VAR " \"${singleOption}\"\n")
endforeach()
endif()
endforeach()
if(NOT "${CPM_ARGS_UNPARSED_ARGUMENTS}" STREQUAL "")
if(${IS_IN_COMMENT})
string(APPEND PRETTY_OUT_VAR "#")
endif()
string(APPEND PRETTY_OUT_VAR " ")
foreach(CPM_ARGS_UNPARSED_ARGUMENT ${CPM_ARGS_UNPARSED_ARGUMENTS})
string(APPEND PRETTY_OUT_VAR " ${CPM_ARGS_UNPARSED_ARGUMENT}")
endforeach()
string(APPEND PRETTY_OUT_VAR "\n")
endif()
set(${OUT_VAR}
${PRETTY_OUT_VAR}
PARENT_SCOPE
)
endfunction()
================================================
FILE: src/dsp/compressor.h
================================================
// Spectral Compressor: an FFT based compressor
// Copyright (C) 2021-2022 Robbert van der Helm
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include <juce_dsp/juce_dsp.h>
/**
* A compressor that simultaneously performs both upwards and downwards
* compression. Based on `juce::dsp::Compressor`.
*/
template <typename T>
class MultiwayCompressor {
public:
MultiwayCompressor() { update(); }
/**
* The lowest sample value we will try to upwards compress, otherwise we
* could get infinite gain ratios and it would be waste to try to make
* silence louder.
*/
static constexpr T epsilon =
// GCC supports constexpr `std::pow`, MSVC and Clang don't
// std::pow(static_cast<T>(10.0), static_cast<T>(-100 * 0.05));
1e-05;
/**
* The maximum gain value, to minimize ear damage when doing upwards
* compression.
*/
static constexpr T gain_limit = 200;
/**
* Modes for downwards, upwards, or simulteneous upwards and downwards
* compression. In the last mode the multiway deadzone parameter acts as an
* bidirectional offset to the threshold where the compressor doesn't do
* anything.
*/
enum class Mode { downwards, upwards, multiway };
/**
* Set the compressor's mode.
*/
void set_mode(Mode mode) { mode_ = mode; }
/**
* Set the compressor's deadzone when using the multiway mode. Must not be
* negative.
*/
void set_multiway_deadzone(T deadzone_db) {
jassert(deadzone_db >= 0);
multiway_deadzone_db_ = deadzone_db;
update();
}
/**
* Set the compressor's threshold in dB.
*/
void set_threshold(T threshold_db) {
threshold_db_ = threshold_db;
update();
}
/**
* Set the compressor's ratio (must be higher or equal to 1).
*/
void set_ratio(T ratio) {
jassert(ratio >= 1);
ratio_ = ratio;
update();
}
/**
* Set the compressor's attack time in milliseconds.
*/
void set_attack(T attack) {
jassert(attack >= 0);
attack_time_ = attack;
update();
}
/**
* Set the compressor's release time in milliseconds.
*/
void set_release(T release) {
jassert(release >= 0);
release_time_ = release;
update();
}
/**
* Initialize the processor.
*/
void prepare(const juce::dsp::ProcessSpec& spec) {
jassert(spec.sampleRate > 0);
jassert(spec.numChannels > 0);
sample_rate_ = spec.sampleRate;
envelope_filter_.prepare(spec);
update();
reset();
}
/**
* Reset the internal state variables of the processor.
*/
void reset() { envelope_filter_.reset(); }
/**
* Process the input and output samples supplied in the processing context.
*/
template <typename ProcessContext>
void process(const ProcessContext& context) noexcept {
const auto& input_block = context.getInputBlock();
auto& output_block = context.getOutputBlock();
const auto num_channels = output_block.getNumChannels();
const auto num_samples = output_block.getNumSamples();
jassert(input_block.getNumChannels() == num_channels);
jassert(input_block.getNumSamples() == num_samples);
if (context.isBypassed) {
output_block.copyFrom(input_block);
return;
}
for (size_t channel = 0; channel < num_channels; ++channel) {
auto* input_samples = input_block.getChannelPointer(channel);
auto* output_samples = output_block.getChannelPointer(channel);
for (size_t i = 0; i < num_samples; ++i)
output_samples[i] = process_sample(channel, input_samples[i]);
}
}
/**
* Process a single sample.
*/
T process_sample(int channel, T input) {
// Ballistics filter with peak rectifier
T env = envelope_filter_.processSample(channel, input);
// The VCA can push the gain either upwards, downwards, or do nothing
// depending on the settings and the compressor's mode
// TODO: This can be optimized a bit, but it's fine for now
T gain = 1.0;
if (mode_ != Mode::upwards && env > (threshold_ + multiway_deadzone_)) {
// Downwards compression
gain = std::pow((env - multiway_deadzone_) * threshold_inverse_,
ratio_inverse_ - static_cast<T>(1.0));
} else if (mode_ != Mode::downwards && env > epsilon &&
env < (threshold_ - multiway_deadzone_)) {
// Upwards compression
gain = std::pow((env + multiway_deadzone_) * threshold_inverse_,
ratio_inverse_ - static_cast<T>(1.0));
// When levels drop very low crazy things start happening. At that
// point it's best to just cap the gain ratio.
if (gain > gain_limit) {
gain = gain_limit;
}
}
return input * gain;
}
private:
void update() {
// The deadzone acts in both directions, so it needs to be divided by
// two. If multiway mode is not enabled then the deadzone is always set
// to 0 to simplify the calculations.
multiway_deadzone_ =
mode_ == Mode::multiway
? std::abs(static_cast<T>(1.0) - juce::Decibels::decibelsToGain(
multiway_deadzone_db_)) /
static_cast<T>(2.0)
: 0.0;
threshold_ = juce::Decibels::decibelsToGain(threshold_db_,
static_cast<T>(-200.0));
threshold_inverse_ = static_cast<T>(1.0) / threshold_;
ratio_inverse_ = static_cast<T>(1.0) / ratio_;
envelope_filter_.setAttackTime(attack_time_);
envelope_filter_.setReleaseTime(release_time_);
}
Mode mode_ = Mode::downwards;
double sample_rate_ = 44100.0;
T threshold_db_ = 0.0;
T multiway_deadzone_db_ = 0.0;
T ratio_ = 1.0;
T attack_time_ = 1.0;
T release_time_ = 100.0;
T threshold_ = 1.0;
T threshold_inverse_ = 1.0;
/**
* Will always be set to 0 when the mode is not set to multiway regardless
* of the value of `multiway_deadzone_db_`.
*/
T multiway_deadzone_ = 0.0;
T ratio_inverse_ = 1.0;
juce::dsp::BallisticsFilter<T> envelope_filter_;
};
================================================
FILE: src/dsp/stft.h
================================================
// Spectral Compressor: an FFT based compressor
// Copyright (C) 2021-2022 Robbert van der Helm
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <span>
#include <juce_dsp/juce_dsp.h>
#include "../ring.h"
/**
* Process an audio source in the frequency domain using the overlap-add method.
*
* @tparam with_sidechain Whether to also do a parallel analysis on a sidechain
* input source. If this is enabled, then you will be able to analyze an FFT
* buffer from a sidechain source before processing the main signal.
*/
template <bool with_sidechain = false>
class STFT {
public:
/**
* Initialize this processor for the given FFT order.
*
* @param num_channels The number of channels. This should be equal for the
* input, sidechain, and output busses.
* @param fft_order The order of the FFT window. The actual size of the
* window will be `1 << fft_order`.
*/
STFT(size_t num_channels, size_t fft_order)
: fft_window_size(1 << fft_order),
fft_(fft_order),
windowing_function_(
fft_window_size,
juce::dsp::WindowingFunction<float>::WindowingMethod::hann,
// TODO: Or should we leave normalization enabled?
false),
// JUCE's FFT class interleaves the real and imaginary numbers, so
// this buffer should be twice the window size in size
fft_scratch_buffer_(fft_window_size * 2),
input_ring_buffers_(num_channels, RingBuffer<float>(fft_window_size)),
sidechain_ring_buffers_(with_sidechain ? num_channels : 0,
with_sidechain
? RingBuffer<float>(fft_window_size)
: RingBuffer<float>()),
output_ring_buffers_(num_channels,
RingBuffer<float>(fft_window_size)) {}
/**
* The latency introduced by this processor, in samples.
*/
inline int latency_samples() const { return fft_window_size; }
/**
* Process audio using a short term Fourier transform. This involves using
* the input ring buffers to buffer audio, processing that audio in windows,
* adding up those windows in the output ring buffers, and then finally
* writing those outputs to `buffer`'s outputs. The supplied function can be
* used to actually process the data.
*
* @param main_io The current processing cycle's buffers for the main input
* and output busses. This should contain an input and an output bus with
* an equal number of channels for each bus.
* @param windowing_overlap_times How much overlap we should be using in the
* overlap-add process. This should be a power of two.
* @param gain Gain to apply to every processed window before adding it to
* the output. If set to 1.0, no gain will be added.
* @param preprocess_fn A function that receives a window of raw samples
* just before the FFT processing. The windowing function will have
* already applied at this point.
* @param process_fn A function that receives and modifies an FFT buffer.
* The results will be written back to `buffer`'s outputs using the
* overlap-add method at an `fft_window_size` sample delay.
* @param postprocess_fn A function that receives raw samples just after the
* FFT processing but before they are added to the output ring buffers.
* Windowing will have already been applied at this point.
*
* @tparam FPreProcess A function of type `void(std::span<float>& fft,
* size_t channel)`.
* @tparam FProcess A function of type `void(std::span<std::complex<float>>&
* fft, size_t channel)`.
* @tparam FPostProcess A function of type `void(std::span<float>& fft,
* size_t channel)`.
*/
template <typename FPreProcess, typename FProcess, typename FPostProcess>
void process(juce::AudioBuffer<float>& main_io,
int windowing_overlap_times,
float gain,
FPreProcess preprocess_fn,
FProcess process_fn,
FPostProcess postprocess_fn) {
do_process<false, false>(
main_io, main_io, windowing_overlap_times, gain, [](auto&, auto) {},
[]() {}, std::move(preprocess_fn), std::move(process_fn),
std::move(postprocess_fn));
}
/**
* Process audio using a short term Fourier transform. This involves using
* the input ring buffers to buffer audio, processing that audio in windows,
* adding up those windows in the output ring buffers, and then finally
* writing those outputs to `buffer`'s outputs. The supplied function can be
* used to actually process the data.
*
* This version lets you analyze a sidechain signal before processing the
* main signal.
*
* @param main_io The current processing cycle's buffers for the main input
* and output busses. This should contain an input and an output bus with
* an equal number of channels for each bus.
* @param sidechain_io The current processing cycle's buffers for the
* sidechain input busses. This should have the same number of channels as
* `main_io`.
* @param windowing_overlap_times How much overlap we should be using in the
* overlap-add process. This should be a power of two.
* @param gain Gain to apply to every processed window before adding it to
* the output. If set to 1.0, no gain will be added.
* @param sidechain_fn A function that receives an FFT buffer obtained from
* the sidechain signal that can be used for analysis.
* @param post_sidechain_fn A function called after `sidechain_fn` has been
* called for every channel. Can be used to aggregate per-channel data.
* @param preprocess_fn A function that receives a window of raw samples
* just before the FFT processing. The windowing function will have
* already applied at this point.
* @param process_fn A function that receives and modifies an FFT buffer.
* The results will be written back to `buffer`'s outputs using the
* overlap-add method at an `fft_window_size` sample delay.
* @param postprocess_fn A function that receives raw samples just after the
* FFT processing but before they are added to the output ring buffers.
* Windowing will have already been applied at this point.
*
* @tparam FSidechain A function of type `void(const
* std::span<std::complex<float>>& fft, size_t channel)`.
* @tparam FPostSidechain A `void()` function.
* @tparam FPreProcess A function of type `void(std::span<float>& fft,
* size_t channel)`.
* @tparam FProcess A function of type `void(std::span<std::complex<float>>&
* fft, size_t channel)`.
* @tparam FPostProcess A function of type `void(std::span<float>& fft,
* size_t channel)`.
*/
template <typename FSidechain,
typename FPostSidechain,
typename FPreProcess,
typename FProcess,
typename FPostProcess,
typename = std::enable_if_t<with_sidechain>>
void process(juce::AudioBuffer<float>& main_io,
const juce::AudioBuffer<float>& sidechain_io,
int windowing_overlap_times,
float gain,
FSidechain sidechain_fn,
FPostSidechain post_sidechain_fn,
FPreProcess preprocess_fn,
FProcess process_fn,
FPostProcess postprocess_fn) {
do_process<false, true>(main_io, sidechain_io, windowing_overlap_times,
gain, std::move(sidechain_fn),
std::move(post_sidechain_fn),
std::move(preprocess_fn), std::move(process_fn),
std::move(postprocess_fn));
}
/**
* Don't do any processing, but still keep the same amount of latency as if
* we were calling `process()`.
*
* @param main_io The current processing cycle's buffers for the main input
* and output busses. This should contain an input and an output bus with
* an equal number of channels for each bus.
*/
void process_bypassed(juce::AudioBuffer<float>& main_io) {
do_process<true, false>(
main_io, main_io, 1, 1.0f, [](auto&, auto) {}, []() {},
[](auto&, auto) {}, [](auto&, auto) {}, [](auto&, auto) {});
}
/**
* The size of the FFT window used.
*/
const size_t fft_window_size;
private:
/**
* Depending on `with_sidechain`, there are a few different ways to process
* a buffer. To avoid duplication, this function has two `bypassed` and
* `sidechain_active` template constants that control the order through this
* function. These booleans control whether we do any FFT operations at all,
* and whether we touch the read from the sidechain input and call the
* sidechain analysis functions.
*/
template <bool bypassed,
bool sidechain_active,
typename FSidechain,
typename FPostSidechain,
typename FPreProcess,
typename FProcess,
typename FPostProcess>
void do_process(
juce::AudioBuffer<float>& main_io,
[[maybe_unused]] const juce::AudioBuffer<float>& sidechain_io,
int windowing_overlap_times,
float gain,
[[maybe_unused]] FSidechain sidechain_fn,
[[maybe_unused]] FPostSidechain post_sidechain_fn,
FPreProcess preprocess_fn,
FProcess process_fn,
FPostProcess postprocess_fn) {
juce::ScopedNoDenormals noDenormals;
const size_t num_channels =
static_cast<size_t>(main_io.getNumChannels());
const size_t num_samples = static_cast<size_t>(main_io.getNumSamples());
if constexpr (sidechain_active) {
jassert(sidechain_io.getNumChannels() ==
static_cast<int>(num_channels));
jassert(sidechain_io.getNumSamples() ==
static_cast<int>(num_samples));
}
// We'll process audio in lockstep to make it easier to use processors
// that require lookahead and thus induce latency. Every this many
// samples we'll process a new window of input samples. The results will
// be added to the output ring buffers.
const size_t windowing_interval =
fft_window_size / static_cast<size_t>(windowing_overlap_times);
// We process incoming audio in windows of `windowing_interval`, and
// when using non-power of 2 buffer sizes of buffers that are smaller
// than `windowing_interval` it can happen that we have to copy over
// already processed audio before processing a new window
const size_t already_processed_samples = std::min(
num_samples, (windowing_interval -
(input_ring_buffers_[0].pos() % windowing_interval)) %
windowing_interval);
const size_t samples_to_be_processed =
num_samples - already_processed_samples;
const int windows_to_process = std::ceil(
static_cast<float>(samples_to_be_processed) / windowing_interval);
// Since we're processing audio in small chunks, we need to keep track
// of the current sample offset in `buffers` we should use for our
// actual audio input and output
size_t sample_buffer_offset = 0;
// Copying from the input buffer to our input ring buffer, copying from
// our output ring buffer to the output buffer, and clearing the output
// buffer to prevent feedback is always done in sync
if (already_processed_samples > 0) {
for (size_t channel = 0; channel < num_channels; channel++) {
input_ring_buffers_[channel].read_n_from(
main_io.getReadPointer(channel), already_processed_samples);
if (num_windows_processed_ >= windowing_overlap_times) {
output_ring_buffers_[channel].copy_n_to(
main_io.getWritePointer(channel),
already_processed_samples, true);
} else {
main_io.clear(channel, 0, already_processed_samples);
}
if constexpr (sidechain_active) {
sidechain_ring_buffers_[channel].read_n_from(
sidechain_io.getReadPointer(channel),
already_processed_samples);
}
}
sample_buffer_offset += already_processed_samples;
}
// Now if `windows_to_process > 0`, the current ring buffer position
// will align with a window and we can start doing our FFT magic
for (int window_idx = 0; window_idx < windows_to_process;
window_idx++) {
if constexpr (sidechain_active && !bypassed) {
// The sidechain input is only used for analysis
for (size_t channel = 0; channel < num_channels; channel++) {
sidechain_ring_buffers_[channel].copy_last_n_to(
fft_scratch_buffer_.data(), fft_window_size);
windowing_function_.multiplyWithWindowingTable(
fft_scratch_buffer_.data(), fft_window_size);
// TODO: We can skip negative frequencies here, right?
fft_.performRealOnlyForwardTransform(
fft_scratch_buffer_.data(), true);
const std::span<std::complex<float>> fft_buffer(
reinterpret_cast<std::complex<float>*>(
fft_scratch_buffer_.data()),
fft_window_size);
sidechain_fn(fft_buffer, channel);
}
// The user might want to do some aggregation after processing
// every channel
post_sidechain_fn();
}
// This is where the magic happens!
for (size_t channel = 0; channel < num_channels; channel++) {
if constexpr (!bypassed) {
// Depending on what stage of the transformation process
// we're in, our scratch buffer will contain either samples
// or complex frequency bins. The caller should get a chance
// to preprocess the (windowed) samples, process the
// transformed data, and the postprocess the results after
// the windowing function has been applied after the inverse
// transformation.
std::span<float> sample_buffer(fft_scratch_buffer_.data(),
fft_window_size);
std::span<std::complex<float>> fft_buffer(
reinterpret_cast<std::complex<float>*>(
fft_scratch_buffer_.data()),
fft_window_size);
input_ring_buffers_[channel].copy_last_n_to(
fft_scratch_buffer_.data(), fft_window_size);
windowing_function_.multiplyWithWindowingTable(
fft_scratch_buffer_.data(), fft_window_size);
preprocess_fn(sample_buffer, channel);
fft_.performRealOnlyForwardTransform(
fft_scratch_buffer_.data());
process_fn(fft_buffer, channel);
fft_.performRealOnlyInverseTransform(
fft_scratch_buffer_.data());
windowing_function_.multiplyWithWindowingTable(
fft_scratch_buffer_.data(), fft_window_size);
postprocess_fn(sample_buffer, channel);
// After processing the windowed data, we'll add it to our
// output ring buffer with any (automatic) makeup gain
// applied
output_ring_buffers_[channel].add_n_from_in_place(
fft_scratch_buffer_.data(), fft_window_size, gain);
} else {
// TODO: Implement the bypass to copy directly between the
// ring buffers instead of going through the scratch
// buffer
input_ring_buffers_[channel].copy_last_n_to(
fft_scratch_buffer_.data(), windowing_interval);
output_ring_buffers_[channel].read_n_from_in_place(
fft_scratch_buffer_.data(), windowing_interval);
}
}
// We don't copy over anything to the outputs until we processed a
// full buffer
num_windows_processed_ += 1;
// Copy the input audio into our ring buffer and copy the processed
// audio into the output buffer
const size_t samples_to_process_this_iteration = std::min(
windowing_interval, num_samples - sample_buffer_offset);
for (size_t channel = 0; channel < num_channels; channel++) {
input_ring_buffers_[channel].read_n_from(
main_io.getReadPointer(channel) + sample_buffer_offset,
samples_to_process_this_iteration);
if (num_windows_processed_ >= windowing_overlap_times) {
output_ring_buffers_[channel].copy_n_to(
main_io.getWritePointer(channel) + sample_buffer_offset,
samples_to_process_this_iteration, true);
} else {
main_io.clear(channel, sample_buffer_offset,
samples_to_process_this_iteration);
}
if constexpr (sidechain_active) {
sidechain_ring_buffers_[channel].read_n_from(
sidechain_io.getReadPointer(channel) +
sample_buffer_offset,
samples_to_process_this_iteration);
}
}
sample_buffer_offset += samples_to_process_this_iteration;
}
jassert(sample_buffer_offset == num_samples);
}
/**
* The numbers of windows already processed. We use this to reduce clicks by
* not copying over audio to the output during the first
* `windowing_overlap_times` windows.
*/
int num_windows_processed_ = 0;
/**
* The FFT processor.
*/
juce::dsp::FFT fft_;
/**
* We'll process the signal with overlapping windows that are added to each
* other to form the output signal. See `input_ring_buffers` for more
* information on how we'll do this.
*/
juce::dsp::WindowingFunction<float> windowing_function_;
/**
* We need a scratch buffer that can contain `fft_window_size * 2` samples
* for `fft` to work in.
*/
std::vector<float> fft_scratch_buffer_;
/**
* A ring buffer of size `fft_window_size` for every channel. Every
* `windowing_interval` we'll copy the last `fft_window_size` samples to
* `fft_scratch_buffers` using a window function, process it, and then add
* the results to `output_ring_buffers`.
*/
std::vector<RingBuffer<float>> input_ring_buffers_;
/**
* These ring buffers are identical to `input_ring_buffers`, but with data
* from the sidechain input. When sidechaining is enabled, we set the
* compressor thresholds based on the magnitudes from the same FFT analysis
* applied to the sidechain input.
*/
std::vector<RingBuffer<float>> sidechain_ring_buffers_;
/**
* The processed results as described in the docstring of
* `input_ring_buffers`. Samples from this buffer will be written to the
* output.
*/
std::vector<RingBuffer<float>> output_ring_buffers_;
};
================================================
FILE: src/editor.cpp
================================================
// Spectral Compressor: an FFT based compressor
// Copyright (C) 2021-2022 Robbert van der Helm
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "editor.h"
#include "processor.h"
SpectralCompressorEditor::SpectralCompressorEditor(
SpectralCompressorProcessor& p)
: AudioProcessorEditor(&p), processor_(p) {
setSize(400, 300);
}
SpectralCompressorEditor::~SpectralCompressorEditor() {}
//==============================================================================
void SpectralCompressorEditor::paint(juce::Graphics& g) {
// TODO: Replace with something else. Or drop the GUI.
g.fillAll(
getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId));
g.setColour(juce::Colours::white);
g.setFont(15.0f);
g.drawFittedText("Hello World!", getLocalBounds(),
juce::Justification::centred, 1);
}
void SpectralCompressorEditor::resized() {
// TODO
}
================================================
FILE: src/editor.h
================================================
// Spectral Compressor: an FFT based compressor
// Copyright (C) 2021-2022 Robbert van der Helm
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include "processor.h"
class SpectralCompressorEditor : public juce::AudioProcessorEditor {
public:
explicit SpectralCompressorEditor(SpectralCompressorProcessor&);
~SpectralCompressorEditor() override;
void paint(juce::Graphics&) override;
void resized() override;
private:
SpectralCompressorProcessor& processor_;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SpectralCompressorEditor)
};
================================================
FILE: src/processor.cpp
================================================
// Spectral Compressor: an FFT based compressor
// Copyright (C) 2021-2022 Robbert van der Helm
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "processor.h"
#include "editor.h"
using juce::uint32;
constexpr char input_gain_db_param_name[] = "input_gain";
constexpr char output_gain_db_param_name[] = "output_gain";
constexpr char dry_wet_ratio_param_name[] = "mix";
constexpr char auto_makeup_gain_param_name[] = "auto_makeup_gain";
constexpr char dc_filter_param_name[] = "dc_filter";
constexpr char compressor_settings_group_name[] = "compressors";
constexpr char sidechain_active_param_name[] = "sidechain_active";
constexpr char sidechain_exponential_param_name[] = "sidechain_exp";
constexpr char compressor_mode_param_name[] = "compressor_mode";
constexpr char compressor_multiway_deadzone_param_name[] =
"compressor_multiway_deadzone";
constexpr char compressor_ratio_param_name[] = "compressor_ratio";
constexpr char compressor_attack_ms_param_name[] = "compressor_attack";
constexpr char compressor_release_ms_param_name[] = "compressor_release";
constexpr char spectral_settings_group_name[] = "spectral";
constexpr char fft_order_param_name[] = "fft_size";
constexpr char windowing_overlap_order_param_name[] = "windowing_order";
constexpr int fft_order_minimum = 12;
constexpr int fft_order_maximum = 15;
SpectralCompressorProcessor::SpectralCompressorProcessor()
: AudioProcessor(
BusesProperties()
.withInput("Input", juce::AudioChannelSet::stereo(), true)
.withOutput("Output", juce::AudioChannelSet::stereo(), true)
.withInput("Sidechain", juce::AudioChannelSet::stereo(), true)),
mixer_(1 << fft_order_maximum),
parameters_(
*this,
nullptr,
"parameters",
{
std::make_unique<juce::AudioProcessorParameterGroup>(
compressor_settings_group_name,
"Master",
" | ",
std::make_unique<juce::AudioParameterFloat>(
input_gain_db_param_name,
"Input Gain",
juce::NormalisableRange<float>(-50, 50, 0.1),
0,
" dB"),
std::make_unique<juce::AudioParameterFloat>(
output_gain_db_param_name,
"Output Gain",
juce::NormalisableRange<float>(-50, 50, 0.1),
0,
" dB"),
std::make_unique<juce::AudioParameterBool>(
auto_makeup_gain_param_name,
"Auto Makeup Gain",
true),
std::make_unique<juce::AudioParameterBool>(
dc_filter_param_name,
"DC Filter",
true),
std::make_unique<juce::AudioParameterFloat>(
dry_wet_ratio_param_name,
"Mix",
juce::NormalisableRange<float>(0.0, 1.0, 0.01),
1.0,
"%",
juce::AudioProcessorParameter::genericParameter,
[](float value, int /*max_length*/) -> juce::String {
return juce::String(value * 100.0f, 0);
},
[](const juce::String& text) -> float {
return text.getFloatValue() / 100.0f;
})),
std::make_unique<juce::AudioProcessorParameterGroup>(
compressor_settings_group_name,
"Compressors",
" | ",
std::make_unique<juce::AudioParameterBool>(
sidechain_active_param_name,
"Sidechain Active",
false),
std::make_unique<juce::AudioParameterBool>(
sidechain_exponential_param_name,
"Sidechain Exponential",
false),
std::make_unique<juce::AudioParameterChoice>(
compressor_mode_param_name,
"Compressor Mode",
// This should match `MultiwayCompressor::Mode`
juce::StringArray{"Downwards", "Upwards", "Multiway"},
static_cast<int>(
MultiwayCompressor<float>::Mode::multiway)),
std::make_unique<juce::AudioParameterFloat>(
compressor_multiway_deadzone_param_name,
"Multiway Deadzone",
juce::NormalisableRange<float>(0, 15, 0.1),
7,
" dB"),
std::make_unique<juce::AudioParameterFloat>(
compressor_ratio_param_name,
"Ratio",
juce::NormalisableRange<float>(1.0, 300.0, 0.1, 0.25),
50.0),
std::make_unique<juce::AudioParameterFloat>(
compressor_attack_ms_param_name,
"Attack",
juce::NormalisableRange<float>(0.0, 10000.0, 1.0, 0.2),
140.0,
" ms",
juce::AudioProcessorParameter::genericParameter),
std::make_unique<juce::AudioParameterFloat>(
compressor_release_ms_param_name,
"Release",
juce::NormalisableRange<float>(0.0, 10000.0, 1.0, 0.2),
202.0,
" ms",
juce::AudioProcessorParameter::genericParameter)),
std::make_unique<juce::AudioProcessorParameterGroup>(
spectral_settings_group_name,
"Spectral Settings",
" | ",
std::make_unique<juce::AudioParameterInt>(
fft_order_param_name,
"Resolution",
9,
fft_order_maximum,
fft_order_minimum,
"",
[](int value, int /*max_length*/) -> juce::String {
return juce::String(1 << value);
},
[](const juce::String& text) -> int {
return std::log2(text.getIntValue());
}),
std::make_unique<juce::AudioParameterInt>(
windowing_overlap_order_param_name,
"Overlap",
2,
6,
2,
"x",
[&](int value, int /*max_length*/) -> juce::String {
return juce::String(1 << value);
},
[&](const juce::String& text) -> int {
return std::log2(text.getIntValue());
})),
}),
// TODO: Is this how you're supposed to retrieve non-float parameters?
// Seems a bit excessive
input_gain_db_(
*parameters_.getRawParameterValue(input_gain_db_param_name)),
output_gain_db_(
*parameters_.getRawParameterValue(output_gain_db_param_name)),
auto_makeup_gain_(*dynamic_cast<juce::AudioParameterBool*>(
parameters_.getParameter(auto_makeup_gain_param_name))),
dc_filter_(*dynamic_cast<juce::AudioParameterBool*>(
parameters_.getParameter(dc_filter_param_name))),
dry_wet_ratio_(
*parameters_.getRawParameterValue(dry_wet_ratio_param_name)),
sidechain_active_(*dynamic_cast<juce::AudioParameterBool*>(
parameters_.getParameter(sidechain_active_param_name))),
sidechain_exponential_(*dynamic_cast<juce::AudioParameterBool*>(
parameters_.getParameter(sidechain_exponential_param_name))),
compressor_mode_(*dynamic_cast<juce::AudioParameterChoice*>(
parameters_.getParameter(compressor_mode_param_name))),
compressor_multiway_deadzone_(*parameters_.getRawParameterValue(
compressor_multiway_deadzone_param_name)),
compressor_ratio_(
*parameters_.getRawParameterValue(compressor_ratio_param_name)),
compressor_attack_ms_(
*parameters_.getRawParameterValue(compressor_attack_ms_param_name)),
compressor_release_ms_(
*parameters_.getRawParameterValue(compressor_release_ms_param_name)),
compressor_settings_listener_(
[&](const juce::String& /*parameterID*/, float /*newValue*/) {
compressor_settings_changed_ = true;
}),
fft_order_(*dynamic_cast<juce::AudioParameterInt*>(
parameters_.getParameter(fft_order_param_name))),
windowing_overlap_order_(*dynamic_cast<juce::AudioParameterInt*>(
parameters_.getParameter(windowing_overlap_order_param_name))),
process_data_updater_([&]() {
update_and_swap_process_data();
const size_t new_window_size = 1 << fft_order_;
setLatencySamples(new_window_size);
}),
fft_order_listener_(
[&](const juce::String& /*parameterID*/, float /*newValue*/) {
process_data_updater_.triggerAsyncUpdate();
}) {
// TODO: Move the latency computation elsewhere
const size_t new_window_size = 1 << fft_order_;
setLatencySamples(new_window_size);
// XXX: There doesn't seem to be a fool proof way to just iterate over all
// parameters in a group, right?
for (const auto& compressor_param_name :
{sidechain_active_param_name, compressor_mode_param_name,
compressor_multiway_deadzone_param_name, compressor_ratio_param_name,
compressor_attack_ms_param_name, compressor_release_ms_param_name}) {
parameters_.addParameterListener(compressor_param_name,
&compressor_settings_listener_);
}
parameters_.addParameterListener(fft_order_param_name,
&fft_order_listener_);
}
SpectralCompressorProcessor::~SpectralCompressorProcessor() {}
const juce::String SpectralCompressorProcessor::getName() const {
return JucePlugin_Name;
}
bool SpectralCompressorProcessor::acceptsMidi() const {
#if JucePlugin_WantsMidiInput
return true;
#else
return false;
#endif
}
bool SpectralCompressorProcessor::producesMidi() const {
#if JucePlugin_ProducesMidiOutput
return true;
#else
return false;
#endif
}
bool SpectralCompressorProcessor::isMidiEffect() const {
#if JucePlugin_IsMidiEffect
return true;
#else
return false;
#endif
}
double SpectralCompressorProcessor::getTailLengthSeconds() const {
return 0.0;
}
int SpectralCompressorProcessor::getNumPrograms() {
return 1;
}
int SpectralCompressorProcessor::getCurrentProgram() {
return 0;
}
void SpectralCompressorProcessor::setCurrentProgram(int index) {
juce::ignoreUnused(index);
}
const juce::String SpectralCompressorProcessor::getProgramName(int /*index*/) {
return "default";
}
void SpectralCompressorProcessor::changeProgramName(
int /*index*/,
const juce::String& /*newName*/) {}
void SpectralCompressorProcessor::prepareToPlay(
double sampleRate,
int maximumExpectedSamplesPerBlock) {
max_samples_per_block_ =
static_cast<uint32>(maximumExpectedSamplesPerBlock);
// This is used to set the correct 'effective' sample rate on our
// compressors during the processing loop
last_effective_sample_rate_ = 0.0;
// When the latency changes because of an FFT window size change the host
// will restart playback and this function gets called again. In that case
// we don't want to do an explicit update here, because that would defeat
// the whole purpose of doing this atomic swap thing from a background
// thread.
//
// TODO: In practice this doesn't do anything, since `releaseResources()`
// will also have been called at this point
if (!(process_data_.get().stft &&
process_data_.get().stft->fft_window_size ==
static_cast<size_t>(1 << fft_order_))) {
// After initializing the process data we make an explicit call to
// `process_data.get()` to swap the two filters in case we get a
// parameter change before the first processing cycle
update_and_swap_process_data();
process_data_.get();
}
mixer_.prepare(juce::dsp::ProcessSpec{
.sampleRate = sampleRate,
.maximumBlockSize = static_cast<uint32>(maximumExpectedSamplesPerBlock),
.numChannels = static_cast<uint32>(getMainBusNumInputChannels())});
}
void SpectralCompressorProcessor::releaseResources() {
process_data_.clear([](ProcessData& process_data) {
process_data.stft.reset();
process_data.spectral_compressors.clear();
process_data.spectral_compressors.shrink_to_fit();
process_data.spectral_compressor_sidechain_thresholds.clear();
process_data.spectral_compressor_sidechain_thresholds.shrink_to_fit();
});
}
bool SpectralCompressorProcessor::isBusesLayoutSupported(
const BusesLayout& layouts) const {
// We can support any number of channels, as long as the main input, main
// output, and sidechain input have the same number of channels
const juce::AudioChannelSet sidechain_channel_set =
layouts.getChannelSet(true, 1);
return (layouts.getMainInputChannelSet() ==
layouts.getMainOutputChannelSet()) &&
(sidechain_channel_set == layouts.getMainInputChannelSet()) &&
!layouts.getMainInputChannelSet().isDisabled();
}
void SpectralCompressorProcessor::processBlockBypassed(
juce::AudioBuffer<float>& buffer,
juce::MidiBuffer& /*midiMessages*/) {
juce::AudioBuffer<float> main_io = getBusBuffer(buffer, true, 0);
// We need to maintain the same latency when bypassed, so we'll reuse most
// of the processing logic
ProcessData& process_data = process_data_.get();
process_data.stft->process_bypassed(main_io);
}
void SpectralCompressorProcessor::processBlock(
juce::AudioBuffer<float>& buffer,
juce::MidiBuffer& /*midiMessages*/) {
juce::ScopedNoDenormals noDenormals;
juce::AudioBuffer<float> main_io = getBusBuffer(buffer, true, 0);
juce::AudioBuffer<float> sidechain_io = getBusBuffer(buffer, true, 1);
juce::dsp::AudioBlock<float> main_block(main_io);
mixer_.setWetMixProportion(dry_wet_ratio_);
mixer_.pushDrySamples(main_block);
ProcessData& process_data = process_data_.get();
const double effective_sample_rate =
getSampleRate() /
(static_cast<double>(process_data.stft->fft_window_size) /
(1 << windowing_overlap_order_));
const float fft_frequency_increment =
getSampleRate() / process_data.stft->fft_window_size;
const MultiwayCompressor<float>::Mode compressor_mode =
static_cast<MultiwayCompressor<float>::Mode>(
compressor_mode_.getIndex());
// We have two different gain stages: just before the FFT transformations,
// after the FFT transformations (the makeup gain). As part of the makeup
// gain we also compensate for the overlap caused by our windowing. We don't
// need any manual ramps or fades here because that's already included in
// our Hanning windows.
// TODO: We should probably also compensate for different FFT window sizes
const float input_gain =
juce::Decibels::decibelsToGain(static_cast<float>(input_gain_db_));
float makeup_gain =
(1.0f / (1 << windowing_overlap_order_)) *
juce::Decibels::decibelsToGain(static_cast<float>(output_gain_db_));
// Obviously don't apply auto makeup gain when doing upwards compression,
// that will just blow up speakers
if (auto_makeup_gain_) {
makeup_gain *= 1.0f / input_gain;
// FIXME: None of this makes any sense! But it works for our current
// parameters. At some point, come up with a more
// mathematically justified auto gaining algorithm.
if (compressor_mode != MultiwayCompressor<float>::Mode::upwards) {
if (sidechain_active_) {
// Not really sure what makes sense here
// TODO: Take base threshold into account
makeup_gain *= (compressor_ratio_ + 24.0f) / 25.0f;
} else {
// TODO: Make this smarter, make it take all of the compressor
// parameters into account. It will probably start making
// sense once we add parameters for the threshold and
// ratio.
makeup_gain *=
compressor_ratio_ > 1.0
? ((std::log10(compressor_ratio_ * 100.00f) * 200.0f) -
399.0f) *
(input_gain)
: 1.0f;
}
}
}
auto preprocess_fn = [input_gain](std::span<float>& samples,
size_t /*channel*/) {
// We apply the input gain after the windowing, just before the forward
// FFT transformation
// TODO: This could be folded into the windowing function with a FMA
juce::FloatVectorOperations::multiply(samples.data(), input_gain,
samples.size());
};
auto process_fn = [this, compressor_mode, effective_sample_rate,
fft_frequency_increment, &process_data](
std::span<std::complex<float>>& fft, size_t channel) {
// We'll update the compressor settings just before processing if the
// settings have changed or if the sidechaining has been disabled
bool expected = true;
const bool update_compressors_now =
compressor_settings_changed_.compare_exchange_weak(expected, false);
// If any timing related settings change (so the FFT window size or the
// amount of overlap), we'll need to adjust our compressors accordingly.
// Since this process can cause pops and clicks, we only do it when
// necessary.
const bool update_sample_rate_now =
last_effective_sample_rate_ != effective_sample_rate;
last_effective_sample_rate_ = effective_sample_rate;
// We'll compress every FTT bin individually. Bin 0 is the DC offset and
// should be skipped, and the latter half of the FFT bins should be
// processed in the same way as the first half but in reverse order. The
// real and imaginary parts are interleaved, so ever bin spans two
// values in the scratch buffer. We can 'safely' do this cast so we can
// use the STL's complex value functions.
for (size_t compressor_idx = 0;
compressor_idx < process_data.spectral_compressors.size();
compressor_idx++) {
auto& compressor =
process_data.spectral_compressors[compressor_idx];
// We don't have a compressor for the first bin
const size_t bin_idx = compressor_idx + 1;
if (update_compressors_now) {
compressor.set_mode(compressor_mode);
compressor.set_multiway_deadzone(compressor_multiway_deadzone_);
compressor.set_ratio(compressor_ratio_);
compressor.set_attack(compressor_attack_ms_);
compressor.set_release(compressor_release_ms_);
// TODO: The user should be able to configure their own slope
// (or free drawn)
// TODO: Change the calculations so that the base threshold
// parameter is centered around some frequency
// TODO: And we should be doing both upwards and downwards
// compression, OTT-style
if (!sidechain_active_) {
constexpr float base_threshold_dbfs = 0.0f;
const float frequency = fft_frequency_increment * bin_idx;
// This starts at 1 for 0 Hz (DC)
const float octave = std::log2(frequency + 2);
// The 3 dB is to compensate for bin 0
compressor.set_threshold((base_threshold_dbfs + 3.0f) -
(3.0f * octave));
}
}
if (update_sample_rate_now) {
// TODO: This prepare resets the envelope follower, which is not
// what we want. In our own compressor we should have a
// way to just change the sample rate.
// TODO: Now that the timings are compensated for changing
// window intervals, we might not need this to be
// configurable anymore can just leave this fixed at 4x.
compressor.prepare(juce::dsp::ProcessSpec{
// We only process everything once every
// `windowing_interval`, otherwise our attack and release
// times will be all messed up
.sampleRate = effective_sample_rate,
.maximumBlockSize = max_samples_per_block_,
.numChannels =
static_cast<uint32>(getMainBusNumInputChannels())});
}
const float magnitude = std::abs(fft[bin_idx]);
const float compressed_magnitude =
compressor.process_sample(channel, magnitude);
// We need to scale both the imaginary and real components of the
// bins at the start and end of the spectrum by the same value
// TODO: Add stereo linking
const float compression_multiplier =
magnitude != 0.0f ? compressed_magnitude / magnitude : 1.0f;
// Since we're usign the real-only FFT operations we don't need to
// touch the second, mirrored half of the FFT bins
fft[bin_idx] *= compression_multiplier;
}
// TODO: We might need some kind of optional limiting stage to
// be safe
// TODO: We should definitely add a way to recover transients
// from the original input audio, that sounds really good
if (dc_filter_) {
fft[0] = 0;
}
};
auto postprocess_fn = [](std::span<float>& /*samples*/,
size_t /*channel*/) {};
// We'll process the input signal in windows, using overlap-add
if (sidechain_active_) {
process_data.stft->process(
main_io, sidechain_io, 1 << windowing_overlap_order_, makeup_gain,
[&process_data](const std::span<std::complex<float>>& fft,
size_t /*channel*/) {
// If sidechaining is active, we set the compressor thresholds
// based on a sidechain signal. Since compression is already
// ballistics based we don't need any additional smoothing when
// updating those thresholds.
for (size_t compressor_idx = 0;
compressor_idx < process_data.spectral_compressors.size();
compressor_idx++) {
const size_t bin_idx = compressor_idx + 1;
const float magnitude = std::abs(fft[bin_idx]);
// We'll set the compressor threshold based on the
// arithmetic mean of the magnitudes of all channels. As
// a slight premature optimization (sorry) we'll reset
// these magnitudes after using them to avoid the
// conditional here.
process_data.spectral_compressor_sidechain_thresholds
[compressor_idx] += magnitude;
}
},
[this, &process_data,
num_channels = sidechain_io.getNumChannels()]() {
// After adding up the magnitudes for each bin in
// `process_data.spectral_compressor_sidechain_thresholds` we
// want to actually configure the compressor thresholds based on
// the mean across the different channels
for (size_t compressor_idx = 0;
compressor_idx < process_data.spectral_compressors.size();
compressor_idx++) {
const float mean_magnitude =
process_data.spectral_compressor_sidechain_thresholds
[compressor_idx] /
num_channels;
process_data.spectral_compressors[compressor_idx]
.set_threshold(sidechain_exponential_
? mean_magnitude
: juce::Decibels::gainToDecibels(
mean_magnitude));
process_data.spectral_compressor_sidechain_thresholds
[compressor_idx] = 0;
}
},
preprocess_fn, process_fn, postprocess_fn);
} else {
process_data.stft->process(main_io, 1 << windowing_overlap_order_,
makeup_gain, preprocess_fn, process_fn,
postprocess_fn);
}
mixer_.setWetLatency(process_data.stft->latency_samples());
mixer_.mixWetSamples(main_block);
}
bool SpectralCompressorProcessor::hasEditor() const {
return true;
}
juce::AudioProcessorEditor* SpectralCompressorProcessor::createEditor() {
// TODO: Add an editor at some point
// return new SpectralCompressorEditor(*this);
return new juce::GenericAudioProcessorEditor(*this);
}
void SpectralCompressorProcessor::getStateInformation(
juce::MemoryBlock& destData) {
const std::unique_ptr<juce::XmlElement> xml =
parameters_.copyState().createXml();
copyXmlToBinary(*xml, destData);
}
void SpectralCompressorProcessor::setStateInformation(const void* data,
int sizeInBytes) {
const auto xml = getXmlFromBinary(data, sizeInBytes);
if (xml && xml->hasTagName(parameters_.state.getType())) {
parameters_.replaceState(juce::ValueTree::fromXml(*xml));
}
// TODO: Should we do this here, is will `prepareToPlay()` always be called
// between loading presets and audio processing starting?
update_and_swap_process_data();
// TODO: Do parameter listeners get triggered? Or alternatively, can this be
// called during playback (without `prepareToPlay()` being called
// first)?
// TODO: Move the latency computation elsewhere
const size_t new_window_size = 1 << fft_order_;
setLatencySamples(new_window_size);
}
void SpectralCompressorProcessor::update_and_swap_process_data() {
process_data_.modify_and_swap([this](ProcessData& process_data) {
process_data.stft.emplace(getMainBusNumInputChannels(), fft_order_);
// Every FFT bin on both channels gets its own compressor, hooray! The
// `fft_window_size / 2` is because the first bin is the DC offset and
// shouldn't be compressed, and the bins after the Nyquist frequency are
// the same as the first half but in reverse order. The compressor
// settings will be set in `update_compressors()`, which is triggered on
// the next processing cycle by setting `compressor_settings_changed`
// below.
process_data.spectral_compressors.resize(
process_data.stft->fft_window_size / 2);
process_data.spectral_compressor_sidechain_thresholds.resize(
process_data.spectral_compressors.size());
// After resizing the compressors are uninitialized and should be
// reinitialized
compressor_settings_changed_ = true;
last_effective_sample_rate_ = 0.0;
});
}
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() {
return new SpectralCompressorProcessor();
}
================================================
FILE: src/processor.h
================================================
// Spectral Compressor: an FFT based compressor
// Copyright (C) 2021-2022 Robbert van der Helm
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <optional>
#include <juce_audio_processors/juce_audio_processors.h>
#include <juce_dsp/juce_dsp.h>
#include "dsp/compressor.h"
#include "dsp/stft.h"
#include "ring.h"
#include "utils.h"
/**
* All of the buffers, compressors and other miscellaneous object we'll need to
* do our FFT audio processing. This will be used together with
* `AtomicResizable<T>` so it can be resized depending on the current FFT window
* settings.
*/
struct ProcessData {
/**
* This is where the magic happens. Performs the entire STFT and overlap-add
* process for us. See the `STFT` class for more information.
*/
std::optional<STFT<true>> stft;
/**
* This will contain `fft_window_size / 2` compressors. The compressors are
* already multichannel so we don't need a nested vector here. We'll
* compress the magnitude of every FFT bin (`sqrt(i^2 + r^2)`) individually,
* and then scale both the real and imaginary components by the ratio of
* their magnitude and the compressed value. Bin 0 is the DC offset and the
* bins in the second half should be processed the same was as the bins in
* the first half but mirrored.
*/
std::vector<MultiwayCompressor<float>> spectral_compressors;
/**
* When setting compressor thresholds based on a sidechain signal we should
* be taking the average bin magnitudes of all channels. This buffer
* accumulates `spectral_compressors.size()` threshold values while
* iterating over the channels of the sidechain signal so we can then
* average them and configure the compressors based on that.
*/
std::vector<float> spectral_compressor_sidechain_thresholds;
};
class SpectralCompressorProcessor : public juce::AudioProcessor {
public:
SpectralCompressorProcessor();
~SpectralCompressorProcessor() override;
void prepareToPlay(double sampleRate,
int maximumExpectedSamplesPerBlock) override;
void releaseResources() override;
bool isBusesLayoutSupported(const BusesLayout& layouts) const override;
void processBlockBypassed(juce::AudioBuffer<float>& buffer,
juce::MidiBuffer& midiMessages) override;
using AudioProcessor::processBlockBypassed;
void processBlock(juce::AudioBuffer<float>&, juce::MidiBuffer&) override;
using AudioProcessor::processBlock;
juce::AudioProcessorEditor* createEditor() override;
bool hasEditor() const override;
const juce::String getName() const override;
bool acceptsMidi() const override;
bool producesMidi() const override;
bool isMidiEffect() const override;
double getTailLengthSeconds() const override;
int getNumPrograms() override;
int getCurrentProgram() override;
void setCurrentProgram(int index) override;
const juce::String getProgramName(int index) override;
void changeProgramName(int index, const juce::String& newName) override;
void getStateInformation(juce::MemoryBlock& destData) override;
void setStateInformation(const void* data, int sizeInBytes) override;
private:
/**
* (Re)initialize a process data object and all compressors within it for
* the current FFT order on the next audio processing cycle. The inactive
* object we're modifying will be swapped with the active object on the next
* call to `process_data.get()`. This should not be called from the audio
* thread.
*/
void update_and_swap_process_data();
/**
* This contains all of our scratch buffers, ring buffers, compressors, and
* everything else that depends on the FFT window size.
*/
AtomicallySwappable<ProcessData> process_data_;
/**
* A dry-wet mixer we'll use to be able to blend the processed and the
* unprocessed signals.
*/
juce::dsp::DryWetMixer<float> mixer_;
/**
* Will be set during `prepareToPlay()`, needed to initialize compressors
* when resizing our buffers.
*/
juce::uint32 max_samples_per_block_ = 0;
/**
* The 'effective sample rate' (sample rate divided by the windowing
* interval) for the last processing cycle. If this changes, then we'll need
* to adjust our compressors accordingly.
*/
double last_effective_sample_rate_ = 0.0;
juce::AudioProcessorValueTreeState parameters_;
/**
* This is applied after windowing, just before the forward FFT
* transformation.
*/
std::atomic<float>& input_gain_db_;
/**
* This is essentially the makeup gain, in dB. When automatic makeup gain is
* enabled this is added on top of that.
*/
std::atomic<float>& output_gain_db_;
/**
* Try to automatically compensate for low thresholds. Doesn't do anything
* when sidechaining is active.
*/
juce::AudioParameterBool& auto_makeup_gain_;
/**
* Set the DC bin to 0 when enabled. This tends to otherwise just make the
* signal a lot louder.
*/
juce::AudioParameterBool& dc_filter_;
/**
* How much of the dry signal to mix in with the processed signal. This
* mixing is done after applying the output gain.
*/
std::atomic<float>& dry_wet_ratio_;
/**
* If true, set the compressor thresholds based on the sidechain signal.
*/
juce::AudioParameterBool& sidechain_active_;
/**
* If set to true, the compressor will be configured based on raw
* magnitudes, even though it expects decibels. This is incorrect, but it
* does result in some interesting sound design possibilities.
*
* TODO: Decide on if we want to keep this
*/
juce::AudioParameterBool& sidechain_exponential_;
// TODO: Add threshold offsets, right now you need to do the oldschool
// compressor thing of simultaneously modifying the input and output
// gains
/**
* The compressor's mode. Either downwards, upwards, or simultaneous upwards
* and downwards compression.
*/
juce::AudioParameterChoice& compressor_mode_;
/**
* The range in dB around the threshold in decibel where the compressor
* should not be active when the compressor is set to multiway mode.
*/
std::atomic<float>& compressor_multiway_deadzone_;
/**
* Compressor ratio, where everything above 1.0 means that the signal will
* be compressed above the threshold.
*/
std::atomic<float>& compressor_ratio_;
/**
* Compressor release time in milliseconds.
*/
std::atomic<float>& compressor_attack_ms_;
/**
* Compressor attack time in milliseconds.
*/
std::atomic<float>& compressor_release_ms_;
/**
* Will cause the compressor settings to be updated on the next processing
* cycle whenever a compressor parameter changes.
*/
LambdaParameterListener compressor_settings_listener_;
/**
* Will be set in `CompressorSettingsListener` when any of the compressor
* related settings change so we can update our compressors. We'll
* initialize this to true so the compressors will be initialized during the
* first processing cycle.
*/
std::atomic_bool compressor_settings_changed_ = true;
/**
* The order (where `fft_window_size = 1 << fft_order`) for our spectral
* operations. When this gets changed, we'll resize all of our buffers and
* atomically swap the current and the resized buffers.
*/
juce::AudioParameterInt& fft_order_;
/**
* The order of the overlap for the windowing (where
* `windowing_overlap_times = 1 1 << windowing_overlap_order`). We end up
* processing the signal in `fft_window_size` windows every `fft_window_size
* / windowing_overlap_times` samples. When this setting gets changed, we'll
* also have to update our compressors since the effective sample rate also
* changes.
*/
juce::AudioParameterInt& windowing_overlap_order_;
/**
* Atomically resizes the object `ProcessData` from a background thread.
*/
LambdaAsyncUpdater process_data_updater_;
/**
* When the FFT order parameter changes, we'll have to create a new
* `ProcessData` object for the new FFT window size (or rather, resize an
* inactive one to match the new size).
*/
LambdaParameterListener fft_order_listener_;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SpectralCompressorProcessor)
};
================================================
FILE: src/ring.h
================================================
// Spectral Compressor: an FFT based compressor
// Copyright (C) 2021-2022 Robbert van der Helm
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <juce_audio_basics/juce_audio_basics.h>
#include <juce_core/juce_core.h>
/**
* A simple resizeable ring buffer that allows copying up to its size number of
* samples to and from the buffer at a time.
*
* @tparam T The element type of this ring buffer. Because of the operations
* used, this can only be `float` or `double`.
*/
template <typename T>
class RingBuffer {
public:
/**
* The default constructor doesn't initialize the ring buffer.
* `RingBuffer::resize()` should be called before actually using this.
*
* @see RingBuffer::resize()
*/
RingBuffer() {}
/**
* Initialize the ring buffer to contain `size` `T`s.
*/
RingBuffer(size_t size) : buffer_(size, 0.0) {}
/**
* Resize the ring buffer to be able to contain `new_size` elements. This
* will reset the current position to 0. Existing data will not be cleared.
*/
void resize(size_t new_size) {
buffer_.resize(new_size);
current_pos_ = 0;
}
/**
* Returns the ring buffer's current size.
*/
inline size_t size() const { return buffer_.size(); }
/**
* Returns the current head position in the ring buffer.
*/
inline size_t pos() const { return current_pos_; }
/**
* Copy `num` samples from `src` into the ring buffer, starting at `pos()`.
*
* This advances the current position by `num`.
*
* @param src The buffer to copy from.
* @param num How many elements to read, should not exceed `size()`.
*
* @return The number of elements read.
*
* @throw std::invalid_argument When `num > size()`.
*/
size_t read_n_from(const T* src, size_t num) {
if (num > buffer_.size()) {
throw std::invalid_argument(
"num > size() in RingBuffer::read_n_from()");
}
const auto& [num_to_end, num_from_start] =
split_range_from(current_pos_, num);
std::copy_n(src, num_to_end, &buffer_[current_pos_]);
std::copy_n(src + num_to_end, num_from_start, &buffer_[0]);
current_pos_ += num;
if (current_pos_ >= buffer_.size()) {
current_pos_ -= buffer_.size();
}
return num;
}
/**
* Copy `num` samples (starting at `pos()`) to `dst`.
*
* This advances the current position by `num`.
*
* @param dst Where to write the data to.
* @param num How many elements to write, should not exceed `size()`.
* @param clear Whether we should overwrite the in our buffer we just copied
* to `dst` with `0.0`. We'll use this when writing processed output back
* since we're adding up the results of overlapping processed regions and
* we definitely don't want any feedback.
*
* @return The number of elements copied.
*
* @throw std::invalid_argument When `num > size()`.
*/
size_t copy_n_to(T* dst, size_t num, bool clear) {
if (num > buffer_.size()) {
throw std::invalid_argument(
"num > size() in RingBuffer::copy_n_to()");
}
const auto& [num_to_end, num_from_start] =
split_range_from(current_pos_, num);
std::copy_n(&buffer_[current_pos_], num_to_end, dst);
std::copy_n(&buffer_[0], num_from_start, dst + num_to_end);
if (clear) {
std::fill_n(&buffer_[current_pos_], num_to_end, 0.0);
std::fill_n(&buffer_[0], num_from_start, 0.0);
}
current_pos_ += num;
if (current_pos_ >= buffer_.size()) {
current_pos_ -= buffer_.size();
}
return num;
}
// The following operations are similar to the reading and writing functions
// above, but they do not move the current position
/**
* Add `num` samples from `src` to the existing values in the ring buffer,
* starting at `pos()`. We use this to process overlapping windows. The
* `clear` flag in `copy_n_to()` is used to prevent these additions from
* causing feedback.
*
* This does not advance the current position.
*
* @param src The buffer to copy from.
* @param num How many elements to read, should not exceed `size()`.
* @param gain A gain multiplier before adding the values. If set to `1.0`
* then no gain will be added.
*
* @return The number of elements copied.
*
* @throw std::invalid_argument When `num > size()`.
*/
size_t add_n_from_in_place(const T* src, size_t num, float gain = 1.0) {
if (num > buffer_.size()) {
throw std::invalid_argument(
"num > size() in RingBuffer::copy_n_to()");
}
const auto& [num_to_end, num_from_start] =
split_range_from(current_pos_, num);
if (gain == 1.0) {
juce::FloatVectorOperations::add(&buffer_[current_pos_], src,
num_to_end);
juce::FloatVectorOperations::add(&buffer_[0], src + num_to_end,
num_from_start);
} else {
juce::FloatVectorOperations::addWithMultiply(&buffer_[current_pos_],
src, gain, num_to_end);
juce::FloatVectorOperations::addWithMultiply(
&buffer_[0], src + num_to_end, gain, num_from_start);
}
return num;
}
/**
* Copy `num` samples from `src` to the ring buffer, starting at `pos()`.
* This is only used when the plugin is bypassed to maintain the proper
* latency.
*
* This does not advance the current position.
*
* @param src The buffer to copy from.
* @param num How many elements to read, should not exceed `size()`.
*
* @return The number of elements copied.
*
* @throw std::invalid_argument When `num > size()`.
*/
size_t read_n_from_in_place(const T* src, size_t num) {
if (num > buffer_.size()) {
throw std::invalid_argument(
"num > size() in RingBuffer::copy_n_to()");
}
const auto& [num_to_end, num_from_start] =
split_range_from(current_pos_, num);
std::copy_n(src, num_to_end, &buffer_[current_pos_]);
std::copy_n(src + num_to_end, num_from_start, &buffer_[0]);
return num;
}
/**
* Copy the _last_ `num` samples (going backwards at `pos()`) written to
* this ring buffer to `dst`. In our case we'll likely read the entire ring
* buffer at once (i.e. `num == size()`).
*
* This does not advance the current position.
*
* @param dst Where to write the data to.
* @param num How many elements to write, should not exceed `size()`.
*
* @return The number of elements copied.
*
* @throw std::invalid_argument When `num > size()`.
*/
size_t copy_last_n_to(T* dst, size_t num) {
if (num > buffer_.size()) {
throw std::invalid_argument(
"num > size() in RingBuffer::copy_n_to()");
}
// Like all other C-family languages, the % operator is a remainder
// operator instead of a modulus so you need this abomination when
// dealing with negative numbers
const size_t start_pos =
(current_pos_ - num + buffer_.size()) % buffer_.size();
const auto& [num_to_end, num_from_start] =
split_range_from(start_pos, num);
std::copy_n(&buffer_[start_pos], num_to_end, dst);
std::copy_n(&buffer_[0], num_from_start, dst + num_to_end);
return num;
}
private:
/**
* Returns how to split the range when reading or writing `num` elements
* starting at `from` in this buffer:
*
* ```cpp
* const auto& [num_to_end, num_from_start] =
* split_range_from(pos, num);
* // Do something with buffer[pos, pos + num_to_end]
* // Do something with buffer[0, num_from_start] if num_from_start > 0
* ```
*/
std::pair<size_t, size_t> split_range_from(size_t from, size_t num) {
const size_t num_to_end = std::min(num, buffer_.size() - from);
return std::pair(num_to_end, num - num_to_end);
}
std::vector<T> buffer_;
size_t current_pos_ = 0;
};
================================================
FILE: src/utils.cpp
================================================
// Spectral Compressor: an FFT based compressor
// Copyright (C) 2021-2022 Robbert van der Helm
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "utils.h"
LambdaAsyncUpdater::LambdaAsyncUpdater(fu2::unique_function<void()> callback)
: callback_(std::move(callback)) {}
void LambdaAsyncUpdater::handleAsyncUpdate() {
callback_();
}
LambdaParameterListener::LambdaParameterListener(
fu2::unique_function<void(const juce::String&, float)> callback)
: callback_(std::move(callback)) {}
void LambdaParameterListener::parameterChanged(const juce::String& parameterID,
float newValue) {
callback_(parameterID, newValue);
}
================================================
FILE: src/utils.h
================================================
// Spectral Compressor: an FFT based compressor
// Copyright (C) 2021-2022 Robbert van der Helm
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <juce_audio_processors/juce_audio_processors.h>
#include <function2/function2.hpp>
/**
* Run some function on the message thread. This function will be executed
* synchronously and should thus run in constant time.
*/
class LambdaAsyncUpdater : public juce::AsyncUpdater {
public:
LambdaAsyncUpdater(fu2::unique_function<void()> callback);
void handleAsyncUpdate() override;
private:
fu2::unique_function<void()> callback_;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(LambdaAsyncUpdater)
};
/**
* Run some function whenever a parameter changes. This function will be
* executed synchronously and should thus run in constant time.
*/
class LambdaParameterListener
: public juce::AudioProcessorValueTreeState::Listener {
public:
LambdaParameterListener(
fu2::unique_function<void(const juce::String&, float)> callback);
void parameterChanged(const juce::String& parameterID,
float newValue) override;
private:
fu2::unique_function<void(const juce::String&, float)> callback_;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(LambdaParameterListener)
};
/**
* A wrapper around some `T` that contains an active `T` and an inactive `T`,
* with a pointer pointing to the currently active object. When some plugin
* parameter changes that would require us to resize the object, we can resize
* the inactive object and then swap the two pointers on the next time we fetch
* the object from the audio processing loop. This prevents locking and memory
* allocations on the audio thread. Keep in mind that the active and the
* inactive objects have no relation to each other, and might thus contain
* completely different data.
*/
template <typename T>
class AtomicallySwappable {
public:
/**
* Default initalizes the objects.
*/
AtomicallySwappable()
: pointers_(Pointers{.active = &primary_, .inactive = &secondary_}),
primary_(),
secondary_() {}
/**
* Initialize the objects with some default value.
*
* @param initial The initial value for the object. This will also be copied
* to the inactive slot.
*/
AtomicallySwappable(T initial)
: pointers_(Pointers{.active = &primary_, .inactive = &secondary_}),
primary_(initial),
secondary_(initial) {}
/**
* Return a reference to currently active object. This should be done once
* at the start of the audio processing function, and the same reference
* should be reused for the remainder of the function.
*/
T& get() {
// We'll swap the pointer on the audio thread so that two resizes in a
// row in between audio processing calls don't cause weird behaviour
bool expected = true;
if (needs_swap_.compare_exchange_strong(expected, false)) {
// The CaS should be atomic, even though GCC will always return
// false for the `is_lock_free()`/`is_always_lock_free()` on 128-bit
// types
static_assert(sizeof(Pointers) == sizeof(T* [2]));
Pointers current_pointers, updated_pointers;
do {
current_pointers = pointers_;
updated_pointers =
Pointers{.active = current_pointers.inactive,
.inactive = current_pointers.active};
} while (!pointers_.compare_exchange_weak(current_pointers,
updated_pointers));
}
return *pointers_.load().active;
}
/**
* Modify the inactive object using the supplied function, and swap the
* active and the inactive objects on the next call to `get()`. This may
* block and should thus never be called from the audio thread.
*
* @tparam F A function with the signature `void(T&)`.
*/
template <typename F>
void modify_and_swap(F modify_fn) {
// In case two mutations are performed in a row, we don't want the audio
// thread swapping the objects while we're modifying that same object
// from another thread
num_resizing_threads_.fetch_add(1);
needs_swap_ = false;
std::lock_guard lock(resize_mutex_);
modify_fn(*pointers_.load().inactive);
// If for whatever reason multiple threads are calling this function at
// the same time, then only the last one may set the swap flag to
// prevent (admittedly super rare) data races
if (num_resizing_threads_.fetch_sub(1) == 1) {
needs_swap_ = true;
}
}
/**
* Resize both objects down to their smallest size using the supplied
* function. This should only ever be called from
* `AudioProcessor::releaseResources()`.
*
* @tparam F A function with the signature `void(T&)`.
*/
template <typename F>
void clear(F clear_fn) {
std::lock_guard lock(resize_mutex_);
clear_fn(primary_);
clear_fn(secondary_);
}
private:
/**
* In the unlikely situation that two threads are calling resize at the same
* time, we'll use a mutex to make sure that those two resizes aren't
* happening at the same time and we use this `num_resizing_threads` to make
* sure that `needs_swap` only gets set to `true` when both threads are
* done. This is to prevent a (super rare) race condition where the audio
* thread will CaS `needs_swap` to false and swap the active pointer while
* at the same time another who just got access to the resize mutex is
* working on the now active object.
*/
std::atomic_int num_resizing_threads_ = 0;
std::mutex resize_mutex_;
struct Pointers {
T* active;
T* inactive;
};
std::atomic_bool needs_swap_ = false;
std::atomic<Pointers> pointers_;
T primary_;
T secondary_;
};
gitextract_bzp8s3kq/
├── .clang-format
├── .github/
│ └── workflows/
│ └── build.yml
├── .gitignore
├── CMakeLists.txt
├── COPYING
├── README.md
├── cmake/
│ └── CPM.cmake
└── src/
├── dsp/
│ ├── compressor.h
│ └── stft.h
├── editor.cpp
├── editor.h
├── processor.cpp
├── processor.h
├── ring.h
├── utils.cpp
└── utils.h
SYMBOL INDEX (11 symbols across 5 files)
FILE: src/dsp/compressor.h
function Mode (line 50) | enum class Mode { downwards, upwards, multiway };
FILE: src/editor.h
function class (line 21) | class SpectralCompressorEditor : public juce::AudioProcessorEditor {
FILE: src/processor.h
type ProcessData (line 35) | struct ProcessData {
function class (line 63) | class SpectralCompressorProcessor : public juce::AudioProcessor {
FILE: src/ring.h
function buffer_ (line 43) | RingBuffer(size_t size) : buffer_(size, 0.0) {}
function resize (line 49) | void resize(size_t new_size) {
function read_n_from (line 76) | size_t read_n_from(const T* src, size_t num) {
function copy_n_to (line 111) | size_t copy_n_to(T* dst, size_t num, bool clear) {
function read_n_from_in_place (line 191) | size_t read_n_from_in_place(const T* src, size_t num) {
function copy_last_n_to (line 219) | size_t copy_last_n_to(T* dst, size_t num) {
FILE: src/utils.h
type Pointers (line 172) | struct Pointers {
Condensed preview — 16 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (166K chars).
[
{
"path": ".clang-format",
"chars": 110,
"preview": "BasedOnStyle: Chromium\nIndentWidth: 4\nStandard: Cpp11\n\n# Don't reflow nested comments\nCommentPragmas: '^ *//'\n"
},
{
"path": ".github/workflows/build.yml",
"chars": 5396,
"preview": "name: Automated builds\n\non:\n push:\n branches:\n - '**'\n tags:\n # Run when pushing version tags, since ot"
},
{
"path": ".gitignore",
"chars": 7,
"preview": "build/\n"
},
{
"path": "CMakeLists.txt",
"chars": 4522,
"preview": "cmake_minimum_required(VERSION 3.15)\n\nproject(spectral_compressor VERSION 0.0.1 LANGUAGES CXX)\n\n# TODO: Figure out a cle"
},
{
"path": "COPYING",
"chars": 35149,
"preview": " GNU GENERAL PUBLIC LICENSE\n Version 3, 29 June 2007\n\n Copyright (C) 2007 Free "
},
{
"path": "README.md",
"chars": 1977,
"preview": "# Spectral Compressor\n\n[ 2021-2022 Robbert van der Helm\n//\n// This program is fr"
},
{
"path": "src/dsp/stft.h",
"chars": 20910,
"preview": "// Spectral Compressor: an FFT based compressor\n// Copyright (C) 2021-2022 Robbert van der Helm\n//\n// This program is fr"
},
{
"path": "src/editor.cpp",
"chars": 1538,
"preview": "// Spectral Compressor: an FFT based compressor\n// Copyright (C) 2021-2022 Robbert van der Helm\n//\n// This program is fr"
},
{
"path": "src/editor.h",
"chars": 1185,
"preview": "// Spectral Compressor: an FFT based compressor\n// Copyright (C) 2021-2022 Robbert van der Helm\n//\n// This program is fr"
},
{
"path": "src/processor.cpp",
"chars": 29007,
"preview": "// Spectral Compressor: an FFT based compressor\n// Copyright (C) 2021-2022 Robbert van der Helm\n//\n// This program is fr"
},
{
"path": "src/processor.h",
"chars": 9207,
"preview": "// Spectral Compressor: an FFT based compressor\n// Copyright (C) 2021-2022 Robbert van der Helm\n//\n// This program is fr"
},
{
"path": "src/ring.h",
"chars": 9106,
"preview": "// Spectral Compressor: an FFT based compressor\n// Copyright (C) 2021-2022 Robbert van der Helm\n//\n// This program is fr"
},
{
"path": "src/utils.cpp",
"chars": 1298,
"preview": "// Spectral Compressor: an FFT based compressor\n// Copyright (C) 2021-2022 Robbert van der Helm\n//\n// This program is fr"
},
{
"path": "src/utils.h",
"chars": 6697,
"preview": "// Spectral Compressor: an FFT based compressor\n// Copyright (C) 2021-2022 Robbert van der Helm\n//\n// This program is fr"
}
]
About this extraction
This page contains the full source code of the robbert-vdh/spectral-compressor GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 16 files (157.2 KB), approximately 36.8k tokens, and a symbol index with 11 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.