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$<$: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 $<$:JUCE_DSP_USE_SHARED_FFTW=1> $<$: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. 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. Copyright (C) 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 . 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: Copyright (C) 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 . 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 . ================================================ FILE: README.md ================================================ # Spectral Compressor [![Automated builds](https://github.com/robbert-vdh/spectral-compressor/workflows/Automated%20builds/badge.svg?branch=master&event=push)](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 - (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 . #include /** * A compressor that simultaneously performs both upwards and downwards * compression. Based on `juce::dsp::Compressor`. */ template 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(10.0), static_cast(-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 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(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(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(1.0) - juce::Decibels::decibelsToGain( multiway_deadzone_db_)) / static_cast(2.0) : 0.0; threshold_ = juce::Decibels::decibelsToGain(threshold_db_, static_cast(-200.0)); threshold_inverse_ = static_cast(1.0) / threshold_; ratio_inverse_ = static_cast(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 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 . #pragma once #include #include #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 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::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(fft_window_size)), sidechain_ring_buffers_(with_sidechain ? num_channels : 0, with_sidechain ? RingBuffer(fft_window_size) : RingBuffer()), output_ring_buffers_(num_channels, RingBuffer(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& fft, * size_t channel)`. * @tparam FProcess A function of type `void(std::span>& * fft, size_t channel)`. * @tparam FPostProcess A function of type `void(std::span& fft, * size_t channel)`. */ template void process(juce::AudioBuffer& main_io, int windowing_overlap_times, float gain, FPreProcess preprocess_fn, FProcess process_fn, FPostProcess postprocess_fn) { do_process( 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>& fft, size_t channel)`. * @tparam FPostSidechain A `void()` function. * @tparam FPreProcess A function of type `void(std::span& fft, * size_t channel)`. * @tparam FProcess A function of type `void(std::span>& * fft, size_t channel)`. * @tparam FPostProcess A function of type `void(std::span& fft, * size_t channel)`. */ template > void process(juce::AudioBuffer& main_io, const juce::AudioBuffer& 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(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& main_io) { do_process( 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 void do_process( juce::AudioBuffer& main_io, [[maybe_unused]] const juce::AudioBuffer& 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(main_io.getNumChannels()); const size_t num_samples = static_cast(main_io.getNumSamples()); if constexpr (sidechain_active) { jassert(sidechain_io.getNumChannels() == static_cast(num_channels)); jassert(sidechain_io.getNumSamples() == static_cast(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(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(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> fft_buffer( reinterpret_cast*>( 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 sample_buffer(fft_scratch_buffer_.data(), fft_window_size); std::span> fft_buffer( reinterpret_cast*>( 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 windowing_function_; /** * We need a scratch buffer that can contain `fft_window_size * 2` samples * for `fft` to work in. */ std::vector 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> 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> 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> 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 . #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 . #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 . #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( compressor_settings_group_name, "Master", " | ", std::make_unique( input_gain_db_param_name, "Input Gain", juce::NormalisableRange(-50, 50, 0.1), 0, " dB"), std::make_unique( output_gain_db_param_name, "Output Gain", juce::NormalisableRange(-50, 50, 0.1), 0, " dB"), std::make_unique( auto_makeup_gain_param_name, "Auto Makeup Gain", true), std::make_unique( dc_filter_param_name, "DC Filter", true), std::make_unique( dry_wet_ratio_param_name, "Mix", juce::NormalisableRange(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( compressor_settings_group_name, "Compressors", " | ", std::make_unique( sidechain_active_param_name, "Sidechain Active", false), std::make_unique( sidechain_exponential_param_name, "Sidechain Exponential", false), std::make_unique( compressor_mode_param_name, "Compressor Mode", // This should match `MultiwayCompressor::Mode` juce::StringArray{"Downwards", "Upwards", "Multiway"}, static_cast( MultiwayCompressor::Mode::multiway)), std::make_unique( compressor_multiway_deadzone_param_name, "Multiway Deadzone", juce::NormalisableRange(0, 15, 0.1), 7, " dB"), std::make_unique( compressor_ratio_param_name, "Ratio", juce::NormalisableRange(1.0, 300.0, 0.1, 0.25), 50.0), std::make_unique( compressor_attack_ms_param_name, "Attack", juce::NormalisableRange(0.0, 10000.0, 1.0, 0.2), 140.0, " ms", juce::AudioProcessorParameter::genericParameter), std::make_unique( compressor_release_ms_param_name, "Release", juce::NormalisableRange(0.0, 10000.0, 1.0, 0.2), 202.0, " ms", juce::AudioProcessorParameter::genericParameter)), std::make_unique( spectral_settings_group_name, "Spectral Settings", " | ", std::make_unique( 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( 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( parameters_.getParameter(auto_makeup_gain_param_name))), dc_filter_(*dynamic_cast( parameters_.getParameter(dc_filter_param_name))), dry_wet_ratio_( *parameters_.getRawParameterValue(dry_wet_ratio_param_name)), sidechain_active_(*dynamic_cast( parameters_.getParameter(sidechain_active_param_name))), sidechain_exponential_(*dynamic_cast( parameters_.getParameter(sidechain_exponential_param_name))), compressor_mode_(*dynamic_cast( 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( parameters_.getParameter(fft_order_param_name))), windowing_overlap_order_(*dynamic_cast( 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(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(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(maximumExpectedSamplesPerBlock), .numChannels = static_cast(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& buffer, juce::MidiBuffer& /*midiMessages*/) { juce::AudioBuffer 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& buffer, juce::MidiBuffer& /*midiMessages*/) { juce::ScopedNoDenormals noDenormals; juce::AudioBuffer main_io = getBusBuffer(buffer, true, 0); juce::AudioBuffer sidechain_io = getBusBuffer(buffer, true, 1); juce::dsp::AudioBlock 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(process_data.stft->fft_window_size) / (1 << windowing_overlap_order_)); const float fft_frequency_increment = getSampleRate() / process_data.stft->fft_window_size; const MultiwayCompressor::Mode compressor_mode = static_cast::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(input_gain_db_)); float makeup_gain = (1.0f / (1 << windowing_overlap_order_)) * juce::Decibels::decibelsToGain(static_cast(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::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& 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>& 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(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& /*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>& 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 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 . #pragma once #include #include #include #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` 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; /** * 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> 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 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& buffer, juce::MidiBuffer& midiMessages) override; using AudioProcessor::processBlockBypassed; void processBlock(juce::AudioBuffer&, 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 process_data_; /** * A dry-wet mixer we'll use to be able to blend the processed and the * unprocessed signals. */ juce::dsp::DryWetMixer 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& 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& 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& 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& compressor_multiway_deadzone_; /** * Compressor ratio, where everything above 1.0 means that the signal will * be compressed above the threshold. */ std::atomic& compressor_ratio_; /** * Compressor release time in milliseconds. */ std::atomic& compressor_attack_ms_; /** * Compressor attack time in milliseconds. */ std::atomic& 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 . #pragma once #include #include /** * 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 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 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 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 . #include "utils.h" LambdaAsyncUpdater::LambdaAsyncUpdater(fu2::unique_function callback) : callback_(std::move(callback)) {} void LambdaAsyncUpdater::handleAsyncUpdate() { callback_(); } LambdaParameterListener::LambdaParameterListener( fu2::unique_function 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 . #pragma once #include #include /** * 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 callback); void handleAsyncUpdate() override; private: fu2::unique_function 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 callback); void parameterChanged(const juce::String& parameterID, float newValue) override; private: fu2::unique_function 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 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 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 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_; T primary_; T secondary_; };