This commit is contained in:
Armin 2026-08-15 15:36:28 +02:00
commit 42cf8432bf
57 changed files with 5782 additions and 0 deletions

63
.gitattributes vendored Normal file
View file

@ -0,0 +1,63 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain

17
.gitignore vendored Normal file
View file

@ -0,0 +1,17 @@
# Build output
build/
# CMake generated files
CMakeCache.txt
CMakeFiles/
cmake_install.cmake
*.o
*.a
*.so
*.dylib
# Editor / OS
.DS_Store
.idea/
.vscode/
*.swp

174
CMakeLists.txt Normal file
View file

@ -0,0 +1,174 @@
cmake_minimum_required(VERSION 3.22)
project(Ambivalence VERSION 1.1.0 LANGUAGES C CXX)
# ============================================================================
# C++20
# ============================================================================
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# ============================================================================
# JUCE
# Pass the path to your JUCE checkout with -DJUCE_PATH=/path/to/JUCE,
# or install JUCE in one of the standard locations below.
# ============================================================================
if(NOT DEFINED JUCE_PATH OR JUCE_PATH STREQUAL "")
foreach(_juce_candidate
${CMAKE_CURRENT_SOURCE_DIR}/JUCE
${CMAKE_BINARY_DIR}/JUCE
${CMAKE_HOME_DIRECTORY}/../JUCE
$ENV{HOME}/JUCE
/opt/JUCE
/usr/local/JUCE
/Applications/JUCE)
if(EXISTS "${_juce_candidate}/modules/juce_core")
set(JUCE_PATH "${_juce_candidate}")
break()
endif()
endforeach()
endif()
if(NOT JUCE_PATH)
message(FATAL_ERROR
"JUCE not found. Set -DJUCE_PATH=/path/to/JUCE or install JUCE in a standard location.")
endif()
set(JUCE_PATH "${JUCE_PATH}" CACHE PATH "Path to the JUCE repository" FORCE)
set(_juce_binary_dir "${CMAKE_BINARY_DIR}/JUCE")
if(_juce_binary_dir STREQUAL "${JUCE_PATH}")
set(_juce_binary_dir "${CMAKE_BINARY_DIR}/JUCE-build")
endif()
add_subdirectory(${JUCE_PATH} ${_juce_binary_dir})
# ============================================================================
# plugin definition
# ============================================================================
juce_add_plugin(Ambivalence
COMPANY_NAME "OTODESK"
PLUGIN_MANUFACTURER_CODE "Otdk"
PLUGIN_CODE "Ambi"
FORMATS VST3 Standalone
PRODUCT_NAME "Ambivalence1.1"
PLUGIN_DESCRIPTION "16-channel FDN Algorithmic Reverb"
IS_SYNTH FALSE
NEEDS_MIDI_INPUT FALSE
NEEDS_MIDI_OUTPUT FALSE
IS_MIDI_EFFECT FALSE
EDITOR_WANTS_KEYBOARD_FOCUS FALSE
VST3_AUTO_MANIFEST FALSE
COPY_PLUGIN_AFTER_BUILD FALSE
VERSION "1.1.0"
)
# ============================================================================
# build info (Git branch, commit, dirty state)
# ============================================================================
execute_process(COMMAND git rev-parse --abbrev-ref HEAD
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
OUTPUT_VARIABLE AMBIVALENCE_GIT_BRANCH
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET)
if(NOT AMBIVALENCE_GIT_BRANCH)
set(AMBIVALENCE_GIT_BRANCH "unknown")
endif()
execute_process(COMMAND git rev-parse --short HEAD
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
OUTPUT_VARIABLE AMBIVALENCE_GIT_COMMIT
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET)
if(NOT AMBIVALENCE_GIT_COMMIT)
set(AMBIVALENCE_GIT_COMMIT "unknown")
endif()
execute_process(COMMAND git status --porcelain
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
OUTPUT_VARIABLE AMBIVALENCE_GIT_STATUS
ERROR_QUIET)
if(AMBIVALENCE_GIT_STATUS)
set(AMBIVALENCE_GIT_DIRTY 1)
else()
set(AMBIVALENCE_GIT_DIRTY 0)
endif()
configure_file(Source/BuildInfo.h.in ${CMAKE_CURRENT_BINARY_DIR}/BuildInfo.h @ONLY)
# ============================================================================
# sources (only existing files)
# ============================================================================
target_sources(Ambivalence PRIVATE
${CMAKE_CURRENT_BINARY_DIR}/BuildInfo.h
Source/BuildInfo.h.in
Source/PluginProcessor.h
Source/PluginProcessor.cpp
Source/PluginEditor.h
Source/PluginEditor.cpp
Source/PluginParameters.h
Source/PluginParameters.cpp
Source/PresetManager.h
Source/PresetManager.cpp
Source/GUI/AmbivalenceUI.h
Source/GUI/AmbivalenceUI.cpp
Source/AlgorithmPresets.h
Source/DSP/DSPConstants.h
Source/DSP/DelayMemory.h
Source/DSP/BiquadFilters.h
Source/DSP/BiquadFilters.cpp
Source/DSP/MagnitudeResponseFitter.h
Source/DSP/MagnitudeResponseFitter.cpp
Source/DSP/UniversalEngine.h
Source/DSP/UniversalEngine.cpp
Source/DSP/AcousticMetrics.h
Source/DSP/AcousticMetrics.cpp
Source/GUI/DecayCurveViz.h
Source/GUI/DecayCurveViz.cpp
Source/DSP/Saturator.h
Source/DSP/OutputLimiter.h
Source/DSP/OutputEQ.h
)# ============================================================================
# include paths
# ============================================================================
target_include_directories(Ambivalence PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/Source
${CMAKE_CURRENT_SOURCE_DIR}/Source/DSP
${CMAKE_CURRENT_SOURCE_DIR}/Source/GUI
${CMAKE_CURRENT_BINARY_DIR}
)
# ============================================================================
# JUCE
# ============================================================================
target_link_libraries(Ambivalence
PRIVATE
juce::juce_audio_basics
juce::juce_audio_devices
juce::juce_audio_formats
juce::juce_audio_plugin_client
juce::juce_audio_processors
juce::juce_audio_utils
juce::juce_core
juce::juce_data_structures
juce::juce_dsp
juce::juce_events
juce::juce_graphics
juce::juce_gui_basics
juce::juce_gui_extra
PUBLIC
juce::juce_recommended_config_flags
juce::juce_recommended_lto_flags
juce::juce_recommended_warning_flags
)
# ============================================================================
# compile definitions
# ============================================================================
target_compile_definitions(Ambivalence PRIVATE
JUCE_WEB_BROWSER=0
JUCE_USE_CURL=0
JUCE_VST3_CAN_REPLACE_VST2=0
JUCE_DISPLAY_SPLASH_SCREEN=0
AMBIVALENCE_VERSION="1.1.0"
)
# ============================================================================
# JUCE header generation
# ============================================================================
juce_generate_juce_header(Ambivalence)
# ============================================================================
# build message
# ============================================================================
message(STATUS "===========================================")
message(STATUS "Ambivalence VST3 - CMake Configuration")
message(STATUS "JUCE: ${JUCE_PATH}")
message(STATUS "Build Type: ${CMAKE_BUILD_TYPE}")
message(STATUS "===========================================")

210
LICENSE Normal file
View file

@ -0,0 +1,210 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are 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.
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.
Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software.
A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public.
The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version.
An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license.
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 Affero 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. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph.
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 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 work with which it is combined will remain governed by version 3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of the GNU Affero 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 Affero 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 Affero 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 Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements.
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 AGPL, see <https://www.gnu.org/licenses/>.

141
Makefile Normal file
View file

@ -0,0 +1,141 @@
# ============================================================================
# Ambivalence - build + install (macOS / Linux)
#
# Usage:
# make clean Release build (VST3 + standalone) into build/<platform>
# make install fresh build, remove stale installs, install cleanly
# make uninstall remove installed artifacts
# make clean delete the build directory
# make version print the current git branch + commit
# make info print the resolved configuration
#
# Notes:
# - `make install` always performs a clean rebuild first, so it is fully
# self-sufficient. `make && make install` also works (it builds twice).
# - JUCE is auto-fetched into ./build/JUCE on first build (shallow clone of
# the JUCE_VERSION tag). Override with: make JUCE_PATH=/path/to/JUCE
# - Install targets (user-level, no sudo required):
# macOS: VST3: ~/Library/Audio/Plug-Ins/VST3/Ambivalence1.1.vst3
# Standalone: ~/Applications/Ambivalence1.1.app
# Linux: VST3: ~/.local/lib/vst3/Ambivalence1.1.vst3
# Standalone: ~/.local/bin/Ambivalence1.1
# ============================================================================
PRODUCT := Ambivalence1.1
UNAME_S := $(shell uname -s)
# ============================================================================
# JUCE location
# An explicit JUCE_PATH wins. Otherwise JUCE is shallow-cloned into
# ./build/JUCE (pinned to JUCE_VERSION) so the build is self-contained.
# ============================================================================
JUCE_REPO ?= https://github.com/juce-framework/JUCE.git
JUCE_VERSION ?= 8.0.15
JUCE_SRC ?= $(CURDIR)/build/JUCE
JUCE_PATH ?= $(JUCE_SRC)
# ============================================================================
# Platform-specific settings
# ============================================================================
ifeq ($(UNAME_S),Linux)
BUILD_DIR ?= build/linux
CONFIG ?= Release
JOBS := $(shell nproc 2>/dev/null || echo 1)
PREFIX ?= $(HOME)/.local
VST3_DIR ?= $(HOME)/.local/lib/vst3
APP_DIR := $(PREFIX)/bin
else
BUILD_DIR ?= build/macOS
CONFIG ?= Release
ARCH ?= $(shell uname -m)
ifeq ($(ARCH),universal)
OSX_ARCH := "arm64;x86_64"
else
OSX_ARCH := $(ARCH)
endif
JOBS := $(shell sysctl -n hw.ncpu)
PREFIX ?= $(HOME)
VST3_DIR ?= $(HOME)/Library/Audio/Plug-Ins/VST3
APP_DIR := $(PREFIX)/Applications
endif
CMAKE := cmake
VST3_BUNDLE := $(VST3_DIR)/$(PRODUCT).vst3
STANDALONE := $(APP_DIR)/$(PRODUCT)$(if $(filter $(UNAME_S),Linux),,.app)
ifdef OSX_ARCH
OSX_FLAG := -DCMAKE_OSX_ARCHITECTURES="$(OSX_ARCH)"
endif
.PHONY: all build install uninstall clean version info
all: build
build:
@command -v $(CMAKE) >/dev/null 2>&1 || { echo "error: cmake not found"; exit 1; }
@if [ -d "$(JUCE_PATH)/modules/juce_core" ]; then \
echo "==> JUCE: $(JUCE_PATH)"; \
elif [ "$(JUCE_PATH)" = "$(JUCE_SRC)" ]; then \
echo "==> JUCE: fetching $(JUCE_VERSION) into $(JUCE_SRC)"; \
mkdir -p "$(dir $(JUCE_SRC))"; \
rm -rf "$(JUCE_SRC)"; \
git clone --depth 1 --branch "$(JUCE_VERSION)" "$(JUCE_REPO)" "$(JUCE_SRC)" \
|| { rm -rf "$(JUCE_SRC)"; echo "error: failed to fetch JUCE"; exit 1; }; \
else \
echo "error: JUCE not found at '$(JUCE_PATH)'"; \
echo " run: make JUCE_PATH=/path/to/JUCE"; \
exit 1; \
fi
@echo "==> Ambivalence: clean build ($(CONFIG), $(UNAME_S))"
rm -rf "$(BUILD_DIR)"
$(CMAKE) -S . -B "$(BUILD_DIR)" \
-DJUCE_PATH="$(JUCE_PATH)" \
-DCMAKE_BUILD_TYPE="$(CONFIG)" \
$(OSX_FLAG) \
-DCMAKE_INSTALL_PREFIX="$(PREFIX)" \
-DJUCE_VST3_DIR="$(VST3_DIR)"
$(CMAKE) --build "$(BUILD_DIR)" -j$(JOBS)
install: build
@echo "==> Ambivalence: removing stale installs"
rm -rf "$(VST3_BUNDLE)" "$(STANDALONE)"
@echo "==> Ambivalence: installing"
$(CMAKE) --install "$(BUILD_DIR)"
@echo "==> Done."
@if [ -d "$(VST3_BUNDLE)" ]; then \
echo " VST3: $(VST3_BUNDLE)"; \
else \
echo " WARNING: VST3 bundle not found at $(VST3_BUNDLE)"; \
fi
@if [ -e "$(STANDALONE)" ]; then \
echo " Standalone: $(STANDALONE)"; \
else \
echo " (standalone not installed - checked $(STANDALONE))"; \
fi
@echo " Rescan plugins in your DAW to pick up the new build."
uninstall:
@echo "==> Ambivalence: uninstalling"
rm -rf "$(VST3_BUNDLE)" "$(STANDALONE)"
@echo "==> Done."
clean:
rm -rf "$(BUILD_DIR)"
version:
@echo "branch: $$(git rev-parse --abbrev-ref HEAD)"
@echo "commit: $$(git rev-parse --short HEAD)"
info:
@echo "platform = $(UNAME_S)"
@echo "JUCE_PATH = $(JUCE_PATH)"
@echo "JUCE_REPO = $(JUCE_REPO)"
@echo "JUCE_VER = $(JUCE_VERSION)"
@echo "BUILD_DIR = $(BUILD_DIR)"
@echo "CONFIG = $(CONFIG)"
@echo "JOBS = $(JOBS)"
@echo "PREFIX = $(PREFIX)"
@echo "VST3 = $(VST3_BUNDLE)"
@echo "Standalone = $(STANDALONE)"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
Presets/Deep Tank.ambpreset Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

349
Source/AlgorithmPresets.h Normal file
View file

@ -0,0 +1,349 @@
#pragma once
// ============================================================
// AlgorithmPresets.h FDN Reverb Acoustic Data Library
// 7 algorithms x 10 bands of RT60 / EDT / D50 / C50 / C80
// Bands: 31.25 / 62.5 / 125 / 250 / 500 / 1k / 2k / 4k / 8k / 16k Hz
// ============================================================
#include <array>
namespace FDNReverb {
static constexpr int NUM_BANDS = 10;
static constexpr int NUM_ALGORITHMS = 7;
// Octave-band centre frequencies (Hz)
static constexpr std::array<float, NUM_BANDS> BAND_FREQ = {
31.25f, 62.5f, 125.0f, 250.0f, 500.0f,
1000.0f, 2000.0f, 4000.0f, 8000.0f, 16000.0f
};
struct AcousticData {
std::array<float, NUM_BANDS> rt60; // RT60 (s)
std::array<float, NUM_BANDS> edt; // EDT (s)
std::array<float, NUM_BANDS> d50; // Definition 0-1
std::array<float, NUM_BANDS> c50; // Clarity 50 ms (dB)
std::array<float, NUM_BANDS> c80; // Clarity 80 ms (dB)
};
// -----------------------------------------------------------------------------
// PresetDefaults: when a preset is selected load parameter
// -----------------------------------------------------------------------------
// when a preset is selected, these APVTS parameters are set automatically:
// so the distinctive sound of the preset can be experienced immediately.
// the user can still fine-tune every parameter afterwards.
//
// * v1.1.0 added : saturation
// the saturation harmonic intensity that brings out each preset's character.
// Room1/Room2: subtle (kept clean, restrained warmth)
// Hall1/Hall2: moderate (emphasizes a rich ring)
// Plate: moderate (brings out the metallic character)
// Spring: high (doubles the distinctive metallic character)
// Goldfoil: moderate (balanced)
//
// design rationale:
// - decayTime is the preset's native mid-band (500 Hz) RT60,
// giving decayScale = 1.0 so the source RT curve is reproduced exactly
// - other parameters are empirical values based on each preset's physical traits
// -----------------------------------------------------------------------------
struct PresetDefaults {
float roomSize; // 0.5 ~ 2.0
float decayTime; // seconds ( source preset mid-band RT60)
float hfDamp; // 0.0 ~ 1.0
float lfAbsorb; // 0.0 ~ 1.0
float diffusion; // 0.0 ~ 1.0
float modAmount; // 0.0 ~ 1.0
float modRate; // 0.1 ~ 10 Hz
float erLevel; // 0.0 ~ 1.0
float saturation; // * new in v1.1 : 0.0 ~ 1.0 ( harmonics intensity )
};
// -----------------------------------------------------------------------------
// preset default values
// -----------------------------------------------------------------------------
// index corresponds to algorithmIndex (0=Room1, 1=Room2, ..., 6=Goldfoil)
//
// saturation value rationale:
// - Room1/2 (0.15f/0.20f): natural, clean space; harmonics kept subtle.
// - Hall1/2 (0.30f/0.35f): adds warmth to the rich reverb.
// - Plate (0.40f): metal-plate resonance; moderate harmonics.
// - Spring (0.55f): distinctive metallic character is the core; stronger harmonics highlight it.
// - Goldfoil (0.35f): balanced, between plate and spring.
// -----------------------------------------------------------------------------
static constexpr std::array<PresetDefaults, 7> PRESET_DEFAULTS = { {
// Room1: small live room; decayTime = rt60[4] = 0.21 s
// keep the natural sense of space; saturation minimal (0.00f)
{ 0.85f, 0.21f, 0.55f, 0.45f, 0.55f, 0.20f, 0.40f, 0.70f, 0.00f },
// Room2: medium live room; decayTime = rt60[4] = 1.38 s
// slightly wider than Room1, add a little warmth (0.05f)
{ 1.00f, 1.38f, 0.50f, 0.40f, 0.60f, 0.25f, 0.45f, 0.65f, 0.05f },
// Hall1: medium hall; decayTime = rt60[4] = 1.89 s
// bring out the hall's richness with moderate harmonics (0.10f)
{ 1.30f, 1.89f, 0.45f, 0.35f, 0.70f, 0.30f, 0.30f, 0.60f, 0.10f },
// Hall2: large hall; decayTime = rt60[4] = 2.08 s
// add harmonics for the grandeur of the large space (0.10f)
{ 1.50f, 2.08f, 0.50f, 0.30f, 0.75f, 0.30f, 0.30f, 0.55f, 0.10f },
// Plate: plate reverb (metal plate); decayTime = rt60[4] = 1.1431 s
// the metal-plate resonance is the core; moderate harmonics bring out its character (0.15f)
{ 0.70f, 1.14f, 0.65f, 0.55f, 0.85f, 0.15f, 0.50f, 0.20f, 0.15f },
// Spring: spring decayTime = rt60[4] = 2.9252 s
// distinctive metallic character spring essence . stronger harmonics character (0.20f)
{ 0.50f, 2.93f, 0.70f, 0.60f, 0.65f, 0.26f, 0.33f, 0.10f, 0.20f },
// Goldfoil: gold foil decayTime = rt60[4] = 2.0642 s
// plate spring in between . balanced (0.18f)
{ 0.95f, 2.06f, 0.55f, 0.40f, 0.80f, 0.35f, 0.45f, 0.30f, 0.18f }
} };
// -----------------------------------------------------------------------------
// ERPattern: preset ISM (Image Source Method) ER
// -----------------------------------------------------------------------------
// Allen-Berkley 1979 ISM theory , preset space characteristics
// 12 early reflections .
//
// (delayMs, gain) :
// - delayMs: input delay time ( ms )
// - gain: amplitude (0.0 ~ 1.0)
//
// design principles :
// - distance inverse-square law : gain prop 1/distance prop 1/delayMs
// - wall absorption coefficient ( reflection about -3dB ~ -6dB)
// - physical reflection : floor -> wall -> ceiling -> reflection
//
// Plate/Spring/Goldfoil space ER
// (numTaps = 0). preset ER processing bypass .
// -----------------------------------------------------------------------------
static constexpr int MAX_ER_TAPS = 12;
struct ERTap {
float delayMs;
float gain;
};
struct ERPattern {
int numTaps; // 0 ER bypass
std::array<ERTap, MAX_ER_TAPS> taps;
};
// preset ER
static constexpr std::array<ERPattern, 7> PRESET_ER_PATTERNS = { {
// -------------------------------------------------------------
// Room1: small live room ( about 40 m^3)
// early reflections density , near-field wall surface reflection strong
// -------------------------------------------------------------
{ 12, {{
{ 5.2f, 0.65f }, // floor 1 reflection
{ 8.7f, 0.58f }, // side wall 1 reflection
{ 12.4f, 0.52f }, // side wall 1 reflection
{ 15.8f, 0.46f }, // ceiling 1 reflection
{ 19.3f, 0.42f }, // rear wall 1 reflection
{ 24.1f, 0.36f }, // floor + wall 2 reflection
{ 28.6f, 0.32f }, // wall + ceiling 2 reflection
{ 33.5f, 0.28f }, // 2 reflection
{ 38.9f, 0.24f }, // 2 reflection
{ 45.2f, 0.20f }, // 3 reflection group
{ 52.8f, 0.16f }, // 3 reflection group
{ 62.4f, 0.13f } // 3 reflection group
}}},
// -------------------------------------------------------------
// Room2: medium live room ( about 100 m^3)
// reflection times wide range , ER smooth
// -------------------------------------------------------------
{ 12, {{
{ 7.5f, 0.62f },
{ 12.3f, 0.55f },
{ 17.1f, 0.49f },
{ 22.8f, 0.43f },
{ 28.5f, 0.38f },
{ 34.2f, 0.33f },
{ 41.6f, 0.28f },
{ 49.3f, 0.24f },
{ 57.8f, 0.20f },
{ 67.5f, 0.17f },
{ 78.2f, 0.14f },
{ 90.6f, 0.11f }
}}},
// -------------------------------------------------------------
// Hall1: medium hall ( about 2000 m^3)
// side wall reflection dominant , initial delay longer
// -------------------------------------------------------------
{ 12, {{
{ 12.0f, 0.58f }, // floor 1 reflection
{ 18.5f, 0.52f }, // side wall 1 reflection
{ 25.7f, 0.47f }, // ceiling 1 reflection
{ 33.4f, 0.42f }, // side wall 1 reflection
{ 42.1f, 0.38f }, // rear wall 1 reflection
{ 51.6f, 0.33f }, // 2 reflection
{ 62.3f, 0.29f }, // 2 reflection
{ 73.9f, 0.25f }, // 2 reflection
{ 86.5f, 0.21f }, // 3 reflection
{ 99.8f, 0.18f }, // 3 reflection
{ 113.4f, 0.15f }, // 3 reflection
{ 128.7f, 0.12f } // diffuse reflection
}}},
// -------------------------------------------------------------
// Hall2: large hall ( about 12000 m^3)
// reflection times further , initial delay large
// -------------------------------------------------------------
{ 12, {{
{ 16.5f, 0.55f },
{ 24.8f, 0.50f },
{ 33.7f, 0.45f },
{ 43.5f, 0.40f },
{ 54.2f, 0.36f },
{ 65.8f, 0.32f },
{ 78.4f, 0.28f },
{ 92.1f, 0.24f },
{ 107.3f, 0.21f },
{ 123.6f, 0.18f },
{ 141.2f, 0.15f },
{ 160.5f, 0.12f }
}}},
// -------------------------------------------------------------
// Plate: space (ER bypass )
// -------------------------------------------------------------
{ 0, {{}} },
// -------------------------------------------------------------
// Spring: space (ER bypass )
// -------------------------------------------------------------
{ 0, {{}} },
// -------------------------------------------------------------
// Goldfoil: space (ER bypass )
// -------------------------------------------------------------
{ 0, {{}} }
} };
struct AlgorithmPreset {
const char* name;
const char* description;
float volumeM3; // estimated room volume (0 = non-room)
AcousticData acoustics;
};
// -----------------------------------------------------------------------------
// ROOM 1 : OpenAIR Measured Room
// -----------------------------------------------------------------------------
static constexpr AlgorithmPreset PRESET_ROOM1 = {
"ROOM1", "Real Room 1 (OpenAIR)", 40.0f,
{
// RT60 (s) 31 62 125 250 500 1k 2k 4k 8k 16k
{{ 0.8f, 0.57f, 0.28f, 0.27f, 0.21f, 0.19f, 0.21f, 0.22f, 0.21f, 0.18f }},
// EDT (s)
{{ 0.98f, 0.73f, 0.34f, 0.22f, 0.22f, 0.22f, 0.22f, 0.22f, 0.22f, 0.22f }},
// D50
{{ 0.26f, 0.46f, 0.92f, 0.94f, 0.98f, 0.97f, 0.97f, 0.97f, 0.96f, 0.98f }},
// C50 (dB)
{{ -4.65f, -0.74f, 10.63f, 11.92f, 16.19f, 15.36f, 14.75f, 14.48f, 13.44f, 17.54f }},
// C80 (dB)
{{ -2.33f, 5.82f, 16.5f, 20.03f, 23.87f, 26.0f, 23.59f, 22.54f, 22.92f, 27.99f }}
}
};
// -----------------------------------------------------------------------------
// ROOM 2 : OpenAIR Measured Room
// -----------------------------------------------------------------------------
static constexpr AlgorithmPreset PRESET_ROOM2 = {
"ROOM2", "Real Room 2 (OpenAIR)", 100.0f,
{
{{ 4.46f, 1.53f, 1.65f, 1.59f, 1.38f, 0.94f, 0.93f, 0.87f, 0.67f, 1.54f }},
{{ 1.59f, 1.33f, 1.07f, 1.07f, 1.2f, 1.07f, 0.95f, 0.82f, 0.69f, 0.43f }},
{{ 0.2f, 0.3f, 0.39f, 0.56f, 0.43f, 0.53f, 0.52f, 0.57f, 0.68f, 0.86f }},
{{ -5.99f, -3.59f, -1.96f, 1.05f, -1.16f, 0.51f, 0.41f, 1.21f, 3.23f, 7.73f }},
{{ 0.74f, 0.26f, 3.76f, 3.18f, 1.94f, 3.67f, 4.06f, 5.35f, 7.25f, 12.52f }}
}
};
// -----------------------------------------------------------------------------
// HALL 1 : OpenAIR Measured Hall
// -----------------------------------------------------------------------------
static constexpr AlgorithmPreset PRESET_HALL1 = {
"HALL1", "Real Hall 1 (OpenAIR)", 2000.0f,
{
{{ 3.55f, 2.28f, 2.18f, 2.05f, 1.89f, 1.86f, 1.69f, 1.28f, 0.9f, 5.53f }},
{{ 2.72f, 1.95f, 1.7f, 2.21f, 2.08f, 2.08f, 1.82f, 1.44f, 1.06f, 0.42f }},
{{ 0.06f, 0.19f, 0.25f, 0.12f, 0.15f, 0.19f, 0.2f, 0.33f, 0.41f, 0.83f }},
{{ -12.08f, -6.4f, -4.76f, -8.49f, -7.57f, -6.27f, -6.11f, -3.16f, -1.62f, 6.9f }},
{{ -7.02f, -2.45f, -0.66f, -3.54f, -2.86f, -2.84f, -2.37f, 0.46f, 2.63f, 11.94f }}
}
};
// -----------------------------------------------------------------------------
// HALL 2 : OpenAIR Measured Hall
// -----------------------------------------------------------------------------
static constexpr AlgorithmPreset PRESET_HALL2 = {
"HALL2", "Real Hall 2 (OpenAIR)", 12000.0f,
{
{{ 2.15f, 1.48f, 1.63f, 1.91f, 2.08f, 2.09f, 1.82f, 1.6f, 1.18f, 1.11f }},
{{ 1.83f, 1.45f, 1.45f, 2.34f, 2.22f, 1.96f, 1.83f, 1.7f, 1.45f, 1.19f }},
{{ 0.08f, 0.2f, 0.4f, 0.16f, 0.21f, 0.29f, 0.37f, 0.3f, 0.33f, 0.44f }},
{{ -10.33f, -6.01f, -1.69f, -7.33f, -5.82f, -3.83f, -2.34f, -3.73f, -3.08f, -1.f }},
{{ -2.99f, -3.18f, -0.69f, -3.02f, -3.05f, -0.92f, -0.36f, -0.77f, 0.13f, 2.59f }}
}
};
// -----------------------------------------------------------------------------
// PLATE : Studio Nord Bremen EMT-Style
// -----------------------------------------------------------------------------
static constexpr AlgorithmPreset PRESET_PLATE = {
"PLATE", "Vintage Plate (Studio Nord Bremen)", 0.0f,
{
{{ 4.6257f, 2.4647f, 1.6639f, 1.6039f, 1.1431f, 0.8664f, 0.6561f, 0.4921f, 0.3153f, 0.1973f }},
{{ 4.0113f, 2.1374f, 1.4429f, 1.3909f, 1.1926f, 0.8981f, 0.6476f, 0.5082f, 0.3132f, 0.2154f }},
{{ 0.2421f, 0.3389f, 0.4357f, 0.4841f, 0.3966f, 0.5246f, 0.5914f, 0.7155f, 0.8915f, 0.9695f }},
{{ -0.795f, -0.4236f, -0.286f, -0.2757f, -1.8231f, 0.4282f, 1.6063f, 4.0053f, 9.1464f, 15.0212f }},
{{ 5.9205f, 3.1547f, 2.1296f, 2.0529f, 1.1228f, 3.8402f, 5.8356f, 8.7699f, 14.5235f, 23.4324f }}
}
};
// -----------------------------------------------------------------------------
// SPRING : Studio Nord Bremen Vintage Spring
// -----------------------------------------------------------------------------
static constexpr AlgorithmPreset PRESET_SPRING = {
"SPRING", "Vintage Spring (Studio Nord Bremen)", 0.0f,
{
{{ 10.3635f, 5.522f, 3.7278f, 3.5934f, 2.9252f, 2.7681f, 2.0397f, 2.0373f, 2.1111f, 0.9319f }},
{{ 4.9277f, 2.6256f, 1.7725f, 1.7086f, 1.5756f, 1.875f, 1.3502f, 1.3897f, 1.759f, 0.4177f }},
{{ 0.1261f, 0.1765f, 0.227f, 0.2522f, 0.2939f, 0.1999f, 0.3321f, 0.3724f, 0.3551f, 0.9202f }},
{{ -13.6143f, -7.2541f, -4.8971f, -4.7206f, -3.806f, -6.0225f, -3.0348f, -2.2663f, -2.591f, 10.6169f }},
{{ -3.0172f, -1.6077f, -1.0853f, -1.0462f, -1.6692f, -2.0212f, 0.4169f, 0.5432f, 0.0761f, 13.5636f }}
}
};
// -----------------------------------------------------------------------------
// GOLD FOIL : Studio Nord Bremen Foil Reverb
// -----------------------------------------------------------------------------
static constexpr AlgorithmPreset PRESET_GOLDFOIL = {
"GOLDFOIL", "Gold Foil (Studio Nord Bremen)", 0.0f,
{
{{ 6.3421f, 3.3793f, 2.2813f, 2.1991f, 2.0642f, 2.255f, 2.17f, 1.4604f, 0.8313f, 0.4339f }},
{{ 5.8718f, 3.1287f, 2.1121f, 2.036f, 1.8796f, 2.2713f, 2.0286f, 1.4122f, 0.7486f, 0.3458f }},
{{ 0.1209f, 0.1693f, 0.2177f, 0.2419f, 0.2894f, 0.2261f, 0.2938f, 0.4516f, 0.6304f, 0.8808f }},
{{ -14.3077f, -7.6236f, -5.1466f, -4.9611f, -3.9018f, -5.3429f, -3.8094f, -0.8434f, 2.3195f, 8.6843f }},
{{ -2.5178f, -1.3416f, -0.9057f, -0.873f, -1.4987f, -1.424f, -1.4095f, 1.7577f, 5.4677f, 12.9829f }}
}
};
// -----------------------------------------------------------------------------
// Master table
// -----------------------------------------------------------------------------
static constexpr std::array<const AlgorithmPreset*, NUM_ALGORITHMS> ALL_PRESETS = { {
&PRESET_ROOM1,
&PRESET_ROOM2,
&PRESET_HALL1,
&PRESET_HALL2,
&PRESET_PLATE,
&PRESET_SPRING,
&PRESET_GOLDFOIL
} };
} // namespace FDNReverb

5
Source/BuildInfo.h.in Normal file
View file

@ -0,0 +1,5 @@
#pragma once
#define AMBIVALENCE_GIT_BRANCH "@AMBIVALENCE_GIT_BRANCH@"
#define AMBIVALENCE_GIT_COMMIT "@AMBIVALENCE_GIT_COMMIT@"
#define AMBIVALENCE_GIT_DIRTY @AMBIVALENCE_GIT_DIRTY@

View file

@ -0,0 +1,168 @@
#include "AcousticMetrics.h"
#include <algorithm>
#include <cmath>
namespace FDNReverb {
void AcousticMetrics::prepare(double sr, float windowMs) {
sampleRate = sr;
analysisWindowMs = windowMs;
// sample rate time sample count
samples50ms = static_cast<int>(0.050 * sr);
samples80ms = static_cast<int>(0.080 * sr);
analysisWindowSamples = static_cast<int>(windowMs * 0.001 * sr);
// read from the history buffer size analysis +
size_t bufferSize = static_cast<size_t>(analysisWindowSamples + samples80ms + 64);
energyHistory.assign(bufferSize, 0.0f);
reset();
}
void AcousticMetrics::reset() noexcept {
std::fill(energyHistory.begin(), energyHistory.end(), 0.0f);
historyWritePos = 0;
recent50msEnergy = 0.0;
recent80msEnergy = 0.0;
totalEnergy = 0.0;
energyPeak = 0.0f;
energyPeakPos = 0;
updateCounter = 0;
d50.store(0.0f, std::memory_order_relaxed);
c50.store(0.0f, std::memory_order_relaxed);
c80.store(0.0f, std::memory_order_relaxed);
edt.store(0.0f, std::memory_order_relaxed);
}
void AcousticMetrics::processSample(float sample) noexcept {
if (energyHistory.empty()) return;
const int bufferSize = static_cast<int>(energyHistory.size());
// current sample energy ( squared )
float currentEnergy = sample * sample;
// read from the history buffer
energyHistory[historyWritePos] = currentEnergy;
// update the running sums (50 ms / 80 ms / full window)
// add the current sample, subtract the value from 50 ms ago
const int read50Pos = (historyWritePos - samples50ms + bufferSize) % bufferSize;
const int read80Pos = (historyWritePos - samples80ms + bufferSize) % bufferSize;
const int readWindowPos = (historyWritePos - analysisWindowSamples + bufferSize) % bufferSize;
recent50msEnergy += currentEnergy - energyHistory[read50Pos];
recent80msEnergy += currentEnergy - energyHistory[read80Pos];
totalEnergy += currentEnergy - energyHistory[readWindowPos];
// peak detection (EDT estimate )
if (currentEnergy > energyPeak) {
energyPeak = currentEnergy;
energyPeakPos = historyWritePos;
}
//
historyWritePos = (historyWritePos + 1) % bufferSize;
// value stable ( cumulative value 0 )
if (recent50msEnergy < 0.0) recent50msEnergy = 0.0;
if (recent80msEnergy < 0.0) recent80msEnergy = 0.0;
if (totalEnergy < 0.0) totalEnergy = 0.0;
// value interval update
if (++updateCounter >= kUpdateInterval) {
updateMetrics();
updateCounter = 0;
}
}
void AcousticMetrics::updateMetrics() noexcept {
// 50ms energy
double energy50ToInf = totalEnergy - recent50msEnergy;
if (energy50ToInf < 1e-12) energy50ToInf = 1e-12;
// 80ms energy
double energy80ToInf = totalEnergy - recent80msEnergy;
if (energy80ToInf < 1e-12) energy80ToInf = 1e-12;
// entire energy ( minimum value clipping )
double totalSafe = std::max(1e-12, totalEnergy);
// -- D50 compute (0~1 ) --
float d50val = static_cast<float>(recent50msEnergy / totalSafe);
d50val = std::min(1.0f, std::max(0.0f, d50val));
d50.store(d50val, std::memory_order_relaxed);
// -- C50 compute (dB) --
float c50val = static_cast<float>(10.0 * std::log10(recent50msEnergy / energy50ToInf));
c50val = std::min(60.0f, std::max(-60.0f, c50val));
c50.store(c50val, std::memory_order_relaxed);
// -- C80 compute (dB) --
float c80val = static_cast<float>(10.0 * std::log10(recent80msEnergy / energy80ToInf));
c80val = std::min(60.0f, std::max(-60.0f, c80val));
c80.store(c80val, std::memory_order_relaxed);
// -- EDT estimate (running) --
// after the peak, the time until energy falls to 1/10 (-10 dB decay)
// * exact EDT needs offline IR analysis; here we estimate from the peak decay time
float edtVal = 0.0f;
if (energyPeak > 1e-9f) {
// analysis peak
// scan from the peak sample until the energy reaches 1/10
const int bufferSize = static_cast<int>(energyHistory.size());
int searchStart = energyPeakPos;
float threshold = energyPeak * 0.1f; // 10dB decay
int decaySamples = 0;
for (int i = 1; i < analysisWindowSamples; ++i) {
int pos = (searchStart + i) % bufferSize;
if (energyHistory[pos] < threshold) {
decaySamples = i;
break;
}
}
edtVal = static_cast<float>(decaySamples) / static_cast<float>(sampleRate) * 6.0f;
// * 10 dB decay time x 6 ~= EDT (60 dB decay correction)
}
edt.store(edtVal, std::memory_order_relaxed);
}
// -----------------------------------------------------------------------------
// drawing: get instantaneous energy at a past time offset
// -----------------------------------------------------------------------------
// secondsAgo: how many seconds in the past to look up
// returns: the energy value at that time (squared)
//
// reads the history buffer directly for the GUI.
// out of range returns 0.
// -----------------------------------------------------------------------------
float AcousticMetrics::getEnergyAtTimeOffset(float secondsAgo) const noexcept {
if (energyHistory.empty()) return 0.0f;
const int bufferSize = static_cast<int>(energyHistory.size());
int offsetSamples = static_cast<int>(secondsAgo * static_cast<float>(sampleRate));
// clamp to range
if (offsetSamples < 0) offsetSamples = 0;
if (offsetSamples >= analysisWindowSamples) return 0.0f;
// read from the history buffer
int readPos = (historyWritePos - 1 - offsetSamples + bufferSize) % bufferSize;
return energyHistory[readPos];
}
// -----------------------------------------------------------------------------
// input activity detection: energy over the last 50 ms
// -----------------------------------------------------------------------------
// used by the GUI to hide the "measured line" when inactive.
// threshold: -60 dBFS (1e-6) energy
// -----------------------------------------------------------------------------
bool AcousticMetrics::isActive() const noexcept {
// energy over the last 50 ms determines activity
constexpr double kActivityThreshold = 1e-6; // -60dBFS
return recent50msEnergy > kActivityThreshold;
}
} // namespace FDNReverb

View file

@ -0,0 +1,104 @@
#pragma once
#include "DSPConstants.h"
#include <array>
#include <atomic>
#include <vector> // <- 1 row added
namespace FDNReverb {
// -----------------------------------------------------------------------------
// AcousticMetrics class
// -----------------------------------------------------------------------------
// Computes real-time acoustic metrics (D50, C50, C80, EDT).
//
// Principle:
// Accumulate the squared energy of the input signal in a ring buffer,
// then compare it against the energy from 50 ms / 80 ms ago,
// and compute the D50 / C50 / C80 values.
//
// Sample-rate support:
// times (ms) are converted to sample counts,
// so 44.1 kHz through 192 kHz are supported automatically.
//
// CPU:
// O(1) per-sample computation (energy accumulation)
// CPU overhead: below ~0.5%
// -----------------------------------------------------------------------------
class AcousticMetrics {
public:
AcousticMetrics() = default;
// -- initialize --
// sampleRate: sample rate (Hz)
// analysisWindowMs: analysis window (ms). 2000 ms (2 s)
void prepare(double sampleRate, float analysisWindowMs = 2000.0f);
// -- per-sample state update --
// sample: current Wet signal sample (mono)
void processSample(float sample) noexcept;
// -- value getters --
// ranges:
// D50: 0.0 ~ 1.0 ( 0.3~0.9)
// C50: -10 ~ +30 dB
// C80: -10 ~ +30 dB
// EDT: 0.0 ~ 5.0 (s)
float getD50() const noexcept { return d50.load(std::memory_order_relaxed); }
float getC50() const noexcept { return c50.load(std::memory_order_relaxed); }
float getC80() const noexcept { return c80.load(std::memory_order_relaxed); }
float getEDT() const noexcept { return edt.load(std::memory_order_relaxed); }
// --- added: expose energy history for drawing ---
// get the instantaneous energy (squared) at a time offset in the past
float getEnergyAtTimeOffset(float secondsAgo) const noexcept;
// input activity detection (energy over the last 50 ms)
bool isActive() const noexcept;
// -- reset --
void reset() noexcept;
private:
// -- compute --
void updateMetrics() noexcept;
// -- parameter --
double sampleRate{ 48000.0 };
float analysisWindowMs{ 2000.0f };
// 50ms / 80ms sample count ( sample rate depends on )
int samples50ms{ 2400 }; // @ 48kHz
int samples80ms{ 3840 }; // @ 48kHz
int analysisWindowSamples{ 96000 }; // 2000ms @ 48kHz
// -- buffer --
// energy history ( squared value )
std::vector<float> energyHistory;
int historyWritePos{ 0 };
// -- cumulative energy value --
// 50ms cumulative energy ( time )
double recent50msEnergy{ 0.0 };
// 80ms cumulative energy ( time )
double recent80msEnergy{ 0.0 };
// entire cumulative energy
double totalEnergy{ 0.0 };
// EDT : energy decay tracking
float energyPeak{ 0.0f };
int energyPeakPos{ 0 };
// -- output value (atomic for thread safety) --
std::atomic<float> d50{ 0.0f };
std::atomic<float> c50{ 0.0f };
std::atomic<float> c80{ 0.0f };
std::atomic<float> edt{ 0.0f };
// -- update --
// sample compute ,
// sample interval update
int updateCounter{ 0 };
static constexpr int kUpdateInterval = 1024; // about 21ms @ 48kHz
};
} // namespace FDNReverb

View file

@ -0,0 +1,103 @@
#include "BiquadFilters.h"
#include "MagnitudeResponseFitter.h"
#include <JuceHeader.h>
#include <cmath>
#include <algorithm>
namespace FDNReverb {
namespace FilterDesign {
static float tanPi(float f, double fs) noexcept {
return std::tan(juce::MathConstants<float>::pi * (float)(f / fs));
}
BiquadCoeffs lowShelf(float fcHz, float gainDB, double sampleRate) {
float A = std::pow(10.f, gainDB / 40.f);
float K = tanPi(fcHz, sampleRate);
BiquadCoeffs c;
if (gainDB >= 0.f) {
float norm = 1.f / (1.f + K);
c.b0 = (1.f + A * K) * norm;
c.b1 = (A * K - 1.f) * norm;
c.b2 = 0.f;
c.a1 = (K - 1.f) * norm;
c.a2 = 0.f;
}
else {
c.b0 = (1.f + K / A) / (1.f + K);
c.b1 = (K / A - 1.f) / (1.f + K);
c.b2 = 0.f;
c.a1 = (K - 1.f) / (1.f + K);
c.a2 = 0.f;
}
return c;
}
BiquadCoeffs highShelf(float fcHz, float gainDB, double sampleRate) {
float A = std::pow(10.f, gainDB / 40.f);
float K = tanPi(fcHz, sampleRate);
BiquadCoeffs c;
if (gainDB >= 0.f) {
float norm = 1.f / (1.f + K);
c.b0 = (A + K) * norm;
c.b1 = (K - A) * norm;
c.b2 = 0.f;
c.a1 = (K - 1.f) * norm;
c.a2 = 0.f;
}
else {
float norm = 1.f / (1.f + K);
c.b0 = (1.f + A * K) * norm;
c.b1 = (A * K - 1.f) * norm;
c.b2 = 0.f;
c.a1 = (K - 1.f) * norm;
c.a2 = 0.f;
}
return c;
}
BiquadCoeffs peak(float fcHz, float gainDB, float Q, double sampleRate) {
float A = std::pow(10.f, gainDB / 40.f);
float w0 = 2.f * juce::MathConstants<float>::pi * fcHz / (float)sampleRate;
float alpha = std::sin(w0) / (2.f * Q);
float cos0 = std::cos(w0);
BiquadCoeffs c;
c.a1 = 2.f * cos0 / (1.f + alpha / A);
c.a2 = (1.f - alpha / A) / (1.f + alpha / A);
c.b0 = (1.f + alpha * A) / (1.f + alpha / A);
c.b1 = -2.f * cos0 / (1.f + alpha / A);
c.b2 = (1.f - alpha * A) / (1.f + alpha / A);
return c;
}
BiquadCoeffs highPass1st(float fcHz, double sampleRate) {
float K = tanPi(fcHz, sampleRate);
float n = 1.f + K;
BiquadCoeffs c;
c.b0 = 1.f / n; c.b1 = -1.f / n; c.b2 = 0.f;
c.a1 = (K - 1.f) / n; c.a2 = 0.f;
return c;
}
// -------------------------------------------------------------------------
// designAbsorption: MagnitudeResponseFitter
// -------------------------------------------------------------------------
// keeps the existing (UniversalEngine) helper functions,
// preserving the internal Stage-1 MRF behavior.
//
// old implementation : gain + Low/High cascade
// new implementation : Jot orthogonalizing 1 filter + LF/HF correction
// -------------------------------------------------------------------------
std::array<BiquadCoeffs, ABSO_STAGES> designAbsorption(
int delaySamples, double sampleRate,
const std::array<float, NUM_BANDS>& rt60,
float hfDamping, float lfAbsorption)
{
// MagnitudeResponseFitter processing
auto result = MagnitudeResponseFitter::design(
delaySamples, sampleRate, rt60, hfDamping, lfAbsorption);
return result.coeffs;
}
} // namespace FilterDesign
} // namespace FDNReverb

View file

@ -0,0 +1,43 @@
#pragma once
#include "DSPConstants.h"
#include "../AlgorithmPresets.h"
#include <array>
namespace FDNReverb {
// -----------------------------------------------------------------------------
// Biquad helpers (Direct Form II Transposed - most robust)
// -----------------------------------------------------------------------------
struct BiquadCoeffs {
float b0{ 1.f }, b1{ 0.f }, b2{ 0.f };
float a1{ 0.f }, a2{ 0.f };
};
struct BiquadState {
float s1{ 0.f }, s2{ 0.f };
inline float tick(float x, const BiquadCoeffs& c) noexcept {
float y = c.b0 * x + s1;
s1 = c.b1 * x - c.a1 * y + s2;
s2 = c.b2 * x - c.a2 * y;
return y;
}
void reset() noexcept { s1 = s2 = 0.f; }
};
// -----------------------------------------------------------------------------
// Filter design utilities
// -----------------------------------------------------------------------------
namespace FilterDesign {
BiquadCoeffs lowShelf(float fcHz, float gainDB, double sampleRate);
BiquadCoeffs highShelf(float fcHz, float gainDB, double sampleRate);
BiquadCoeffs peak(float fcHz, float gainDB, float Q, double sampleRate);
BiquadCoeffs highPass1st(float fcHz, double sampleRate);
BiquadCoeffs allpass1st(float fcHz, double sampleRate);
// Design absorption filter cascade for delay lines
// : function internal MagnitudeResponseFitter
std::array<BiquadCoeffs, ABSO_STAGES> designAbsorption(
int delaySamples, double sampleRate,
const std::array<float, NUM_BANDS>& rt60,
float hfDamping, float lfAbsorption);
}
} // namespace FDNReverb

26
Source/DSP/DSPConstants.h Normal file
View file

@ -0,0 +1,26 @@
#pragma once
#include <array>
namespace FDNReverb {
// -- Compile-time constants ----------------------------------------------------
static constexpr int FDN_N = 8; // FDN order (channels; legacy definition kept for reference)
static constexpr int SAPF_STAGES = 3; // allpass stages per delay line
static constexpr int ABSO_STAGES = 3; // Stage 1: Jot first-order + LF/HF correction
static constexpr int ER_TAPS = 16; // early-reflection FIR taps
// Stage 2 (Valimaki-Liski cumulative GEQ) stages:
// 10: 10-band GEQ (interaction matrix + WLS)
//
// important design notes:
// - the mid-band gain (midGain) of GEQ band 0 is absorbed into the b0/b1/b2 coefficients;
// no separate DC gain stage is needed to avoid DC coloration,
// just a single gain.
// - LF Absorption / HF Damping are applied directly as GEQ target dB,
// fully independent of each other.
// - targets are clamped to 0 dB or below, mathematically guaranteeing loop gain <= 1.
static constexpr int ABSO_STAGES_S2 = 10;
// Mutually-prime base delays (samples @ 48 kHz), log-distributed 30-130 ms
static constexpr std::array<int, FDN_N> BASE_PRIMES_48K = {
1451, 1693, 1979, 2311, 2683, 3067, 3491, 3923
};
} // namespace FDNReverb

134
Source/DSP/DelayMemory.h Normal file
View file

@ -0,0 +1,134 @@
#pragma once
#include <vector>
#include <cmath>
#include <algorithm>
#include <cstdint>
namespace FDNReverb {
// -----------------------------------------------------------------------------
// memory pool (Single-Large Buffer)
// -----------------------------------------------------------------------------
class DelayMemoryPool {
public:
void allocate(size_t totalSamples) {
buffer.assign(totalSamples, 0.0f);
allocOffset = 0;
}
// pointer sized up to the next power of two (also outputs an index mask)
float* requestMemory(size_t samplesNeeded, int& outMask) {
size_t powerOfTwoSize = 1;
while (powerOfTwoSize < samplesNeeded) powerOfTwoSize *= 2;
if (allocOffset + powerOfTwoSize > buffer.size()) return nullptr;
float* ptr = buffer.data() + allocOffset;
outMask = static_cast<int>(powerOfTwoSize - 1);
allocOffset += powerOfTwoSize;
return ptr;
}
void clear() { std::fill(buffer.begin(), buffer.end(), 0.0f); }
private:
std::vector<float> buffer;
size_t allocOffset{ 0 };
};
// -----------------------------------------------------------------------------
// interpolation
// -----------------------------------------------------------------------------
class LinearDelayLine {
public:
void init(float* memory, int bitmask) {
buffer = memory;
mask = bitmask;
writeIndex = 0;
}
// linear interpolation ( high band natural Air Absorption )
inline float read(float delayInSamples) const noexcept {
int id = static_cast<int>(delayInSamples);
float frac = delayInSamples - static_cast<float>(id);
// bitwise ops undefined behavior completely , uint32_t
uint32_t uWrite = static_cast<uint32_t>(writeIndex);
uint32_t uId = static_cast<uint32_t>(id);
uint32_t uMask = static_cast<uint32_t>(mask);
int readIdx1 = static_cast<int>((uWrite - uId) & uMask);
int readIdx2 = static_cast<int>((uWrite - uId - 1) & uMask);
return buffer[readIdx1] + frac * (buffer[readIdx2] - buffer[readIdx1]);
}
inline void write(float input) noexcept {
buffer[writeIndex] = input;
writeIndex = (writeIndex + 1) & mask;
}
private:
float* buffer{ nullptr };
int mask{ 0 };
int writeIndex{ 0 };
};
// -----------------------------------------------------------------------------
// Thiran allpass interpolation (preserves the phase response)
// linear interpolation would dull high-band decay (sinc(pi*f) rolloff), so use a Thiran allpass
// which keeps |H(w)| = 1, preserving high-band clarity in the FDN feedback loops.
// -----------------------------------------------------------------------------
class ThiranDelayLine {
public:
void init(float* memory, int bitmask) {
buffer = memory;
mask = bitmask;
writeIndex = 0;
thiranX1 = 0.0f;
thiranY1 = 0.0f;
}
void resetState() noexcept {
thiranX1 = 0.0f;
thiranY1 = 0.0f;
}
// Thiran first-order allpass: y[n] = a*x[n] + x[n-1] - a*y[n-1]
// a = (1-D)/(1+D), D = fractional delay
inline float read(float delayInSamples) noexcept {
int id = static_cast<int>(delayInSamples);
float frac = delayInSamples - static_cast<float>(id);
// clamp below to avoid instability as frac->0, a->1
frac = std::max(frac, 0.1f);
const float a = (1.0f - frac) / (1.0f + frac);
uint32_t uWrite = static_cast<uint32_t>(writeIndex);
uint32_t uId = static_cast<uint32_t>(id);
uint32_t uMask = static_cast<uint32_t>(mask);
float xn = buffer[static_cast<int>((uWrite - uId) & uMask)];
float yn = a * xn + thiranX1 - a * thiranY1;
thiranX1 = xn;
thiranY1 = yn;
return yn;
}
inline void write(float input) noexcept {
buffer[writeIndex] = input;
writeIndex = (writeIndex + 1) & mask;
}
private:
float* buffer{ nullptr };
int mask{ 0 };
int writeIndex{ 0 };
float thiranX1{ 0.0f };
float thiranY1{ 0.0f };
};
} // namespace FDNReverb

View file

@ -0,0 +1,66 @@
#include "EarlyReflections.h"
namespace FDNReverb {
void EarlyReflections::prepare(const juce::dsp::ProcessSpec& spec) {
int maxSamples = (int)(0.7 * spec.sampleRate) + 8;
juce::dsp::ProcessSpec mono = spec;
mono.numChannels = 1;
buf.prepare(mono);
buf.setMaximumDelayInSamples(maxSamples);
erHPCoeffs = FilterDesign::highPass1st(80.f, spec.sampleRate);
float K = std::tan(juce::MathConstants<float>::pi * 6000.f / (float)spec.sampleRate);
erLPCoeffs.b0 = K / (1.f + K);
erLPCoeffs.b1 = erLPCoeffs.b0;
erLPCoeffs.b2 = 0.f;
erLPCoeffs.a1 = (K - 1.f) / (K + 1.f);
erLPCoeffs.a2 = 0.f;
}
void EarlyReflections::buildTaps(const AlgorithmPreset& preset, float roomSizeScale, double sampleRate) {
float erEnergy50 = preset.acoustics.d50[4];
float V = preset.volumeM3 > 0.f ? preset.volumeM3 : 10.f;
float mixTimeMs = std::min(0.0117f * V + 50.1f, 150.f);
float span = mixTimeMs * roomSizeScale;
for (int i = 0; i < ER_TAPS; ++i) {
float t01 = static_cast<float>(i + 1) / static_cast<float>(ER_TAPS);
float delMs = span * std::pow(t01, 1.5f);
taps[i].delaySamples = delMs * 0.001f * (float)sampleRate;
float rt60m = preset.acoustics.rt60[4];
float amp = std::exp(-6.9f * delMs * 0.001f / rt60m);
float factor = (i < ER_TAPS / 2) ? std::sqrt(erEnergy50) : std::sqrt(1.f - erEnergy50);
amp *= factor * std::sqrt(2.f / ER_TAPS);
float pan = (i % 3 == 0) ? -0.707f : ((i % 3 == 1) ? 0.707f : 0.0f);
taps[i].gainL = amp * std::sqrt(0.5f - 0.5f * pan);
taps[i].gainR = amp * std::sqrt(0.5f + 0.5f * pan);
}
}
void EarlyReflections::setPreDelay(float ms, double sampleRate) noexcept {
preDelaySamples = juce::roundToInt(ms * 0.001 * sampleRate);
}
std::pair<float, float> EarlyReflections::tick(float mono) noexcept {
buf.pushSample(0, mono);
float L = 0.f, R = 0.f;
for (const auto& t : taps) {
float d = buf.popSample(0, t.delaySamples + preDelaySamples, false);
L += t.gainL * d;
R += t.gainR * d;
}
L = erHPL.tick(L, erHPCoeffs);
R = erHPR.tick(R, erHPCoeffs);
return { L, R };
}
void EarlyReflections::reset() noexcept {
buf.reset();
erHPL.reset(); erHPR.reset();
erLPL.reset(); erLPR.reset();
}
} // namespace FDNReverb

View file

@ -0,0 +1,32 @@
#pragma once
#include <JuceHeader.h>
#include "DSPConstants.h"
#include "BiquadFilters.h"
namespace FDNReverb {
struct ERTap {
float delaySamples{ 0.f };
float gainL{ 0.f };
float gainR{ 0.f };
};
class EarlyReflections {
public:
void prepare(const juce::dsp::ProcessSpec& spec);
void buildTaps(const AlgorithmPreset& preset, float roomSizeScale, double sampleRate);
void setPreDelay(float ms, double sampleRate) noexcept;
std::pair<float, float> tick(float mono) noexcept;
void reset() noexcept;
private:
juce::dsp::DelayLine<float, juce::dsp::DelayLineInterpolationTypes::Lagrange3rd> buf;
std::array<ERTap, ER_TAPS> taps;
int preDelaySamples{ 0 };
BiquadCoeffs erHPCoeffs, erLPCoeffs;
BiquadState erHPL, erHPR, erLPL, erLPR;
};
} // namespace FDNReverb

View file

@ -0,0 +1,388 @@
#include "MagnitudeResponseFitter.h"
#include <JuceHeader.h>
#include <cmath>
#include <algorithm>
#include <complex>
namespace FDNReverb {
// -----------------------------------------------------------------------------
// static
// -----------------------------------------------------------------------------
std::array<std::array<double, NUM_BANDS>, NUM_BANDS> MagnitudeResponseFitter::cachedB;
std::array<std::array<double, NUM_BANDS>, NUM_BANDS> MagnitudeResponseFitter::cachedBtWB;
std::array<double, NUM_BANDS> MagnitudeResponseFitter::cachedW;
double MagnitudeResponseFitter::cachedSampleRate = 0.0;
bool MagnitudeResponseFitter::cacheValid = false;
// -----------------------------------------------------------------------------
// band Q value ( band : Q ~ sqrt2 / (2^(1/2) - 2^(-1/2)) ~ 1.414)
// -----------------------------------------------------------------------------
static const std::array<float, NUM_BANDS> kBandQs = {
1.7f, // 31.25 Hz (: Q rise )
1.414f, // 62.5 Hz
1.414f, // 125 Hz
1.414f, // 250 Hz
1.414f, // 500 Hz
1.414f, // 1 kHz
1.414f, // 2 kHz
1.414f, // 4 kHz
1.414f, // 8 kHz
1.7f // 16 kHz (: Q rise )
};
const std::array<float, NUM_BANDS>& MagnitudeResponseFitter::getBandQs() noexcept {
return kBandQs;
}
// -----------------------------------------------------------------------------
// Stage 1 ( existing )
// -----------------------------------------------------------------------------
float MagnitudeResponseFitter::t60ToLoopGain(float t60Seconds, int delaySamples, double sampleRate) noexcept {
float t60Safe = std::max(0.01f, t60Seconds);
float exponent = -3.0f * static_cast<float>(delaySamples) / (static_cast<float>(sampleRate) * t60Safe);
return std::pow(10.0f, exponent);
}
float MagnitudeResponseFitter::computeJotPole(float gDC, float alphaRatio) noexcept {
float alphaSafe = juce::jlimit(0.05f, 20.0f, alphaRatio);
float gDCSafe = juce::jlimit(1e-6f, 0.99999f, gDC);
constexpr float kLn10Over4 = 0.5756462732485f;
float log10g = std::log10(gDCSafe);
float alphaSqInv = 1.0f / (alphaSafe * alphaSafe);
float pole = kLn10Over4 * log10g * (1.0f - alphaSqInv);
return juce::jlimit(-0.98f, 0.98f, pole);
}
BiquadCoeffs MagnitudeResponseFitter::orthogonalizedFirstOrderToBiquad(float gain, float pole) noexcept {
BiquadCoeffs c;
c.b0 = gain * (1.0f - pole);
c.b1 = 0.0f;
c.b2 = 0.0f;
c.a1 = -pole;
c.a2 = 0.0f;
return c;
}
float MagnitudeResponseFitter::getT60AtDC(const std::array<float, NUM_BANDS>& rt60) noexcept {
return (rt60[0] + rt60[1]) * 0.5f;
}
float MagnitudeResponseFitter::getT60AtNyquist(const std::array<float, NUM_BANDS>& rt60, double sampleRate) noexcept {
if (sampleRate <= 50000.0) {
return rt60[9];
}
else {
return (rt60[8] + rt60[9]) * 0.5f;
}
}
// -----------------------------------------------------------------------------
// Stage 1 main design function ( existing )
// -----------------------------------------------------------------------------
MagnitudeResponseFitter::DesignResult MagnitudeResponseFitter::design(
int delaySamples,
double sampleRate,
const std::array<float, NUM_BANDS>& rt60,
float hfDamping,
float lfAbsorption)
{
DesignResult result;
float t60DC = std::max(0.01f, getT60AtDC(rt60));
float t60Nyq = std::max(0.01f, getT60AtNyquist(rt60, sampleRate));
float gDC = t60ToLoopGain(t60DC, delaySamples, sampleRate);
float gNyq = t60ToLoopGain(t60Nyq, delaySamples, sampleRate);
float alpha = t60Nyq / t60DC;
float pole = computeJotPole(gDC, alpha);
result.coeffs[0] = orthogonalizedFirstOrderToBiquad(gDC, pole);
float lfShelfDB = -lfAbsorption * 3.0f;
result.coeffs[1] = FilterDesign::lowShelf(150.0f, lfShelfDB, sampleRate);
float hfShelfDB = -hfDamping * 6.0f;
result.coeffs[2] = FilterDesign::highShelf(4000.0f, hfShelfDB, sampleRate);
result.dcGain = gDC;
result.nyquistGain = gNyq;
result.pole = pole;
return result;
}
// -----------------------------------------------------------------------------
// Stage 2 : Biquad peak filter
// -----------------------------------------------------------------------------
BiquadCoeffs MagnitudeResponseFitter::designSymmetricPeakBiquad(
float fcHz, float gainDB, float Q, double sampleRate) noexcept
{
float fcSafe = juce::jlimit(10.0f, static_cast<float>(sampleRate) * 0.49f, fcHz);
float A = std::pow(10.0f, gainDB / 40.0f);
float w0 = 2.0f * juce::MathConstants<float>::pi * fcSafe / static_cast<float>(sampleRate);
float cosW0 = std::cos(w0);
float sinW0 = std::sin(w0);
float alpha = sinW0 / (2.0f * std::max(0.1f, Q));
float a0 = 1.0f + alpha / A;
BiquadCoeffs c;
c.b0 = (1.0f + alpha * A) / a0;
c.b1 = -2.0f * cosW0 / a0;
c.b2 = (1.0f - alpha * A) / a0;
c.a1 = -2.0f * cosW0 / a0;
c.a2 = (1.0f - alpha / A) / a0;
return c;
}
// -----------------------------------------------------------------------------
// Stage 2 : Biquad magnitude response (dB) compute
// -----------------------------------------------------------------------------
float MagnitudeResponseFitter::biquadMagnitudeDB(
const BiquadCoeffs& c, float fEval, double sampleRate) noexcept
{
double w = 2.0 * juce::MathConstants<double>::pi * fEval / sampleRate;
double cosW = std::cos(w);
double sinW = std::sin(w);
double cos2W = std::cos(2.0 * w);
double sin2W = std::sin(2.0 * w);
double bRe = c.b0 + c.b1 * cosW + c.b2 * cos2W;
double bIm = -c.b1 * sinW - c.b2 * sin2W;
double aRe = 1.0 + c.a1 * cosW + c.a2 * cos2W;
double aIm = -c.a1 * sinW - c.a2 * sin2W;
double bMag2 = bRe * bRe + bIm * bIm;
double aMag2 = aRe * aRe + aIm * aIm;
double mag2 = bMag2 / std::max(1e-30, aMag2);
return static_cast<float>(10.0 * std::log10(std::max(1e-30, mag2)));
}
// -----------------------------------------------------------------------------
// Stage 2 : 10x10 LDLT decomposition solver
// -----------------------------------------------------------------------------
void MagnitudeResponseFitter::solveLDLT10(
const std::array<std::array<double, NUM_BANDS>, NUM_BANDS>& A,
const std::array<double, NUM_BANDS>& b,
std::array<double, NUM_BANDS>& x) noexcept
{
constexpr int N = NUM_BANDS;
double L[N][N] = { 0 };
double D[N] = { 0 };
for (int i = 0; i < N; ++i) L[i][i] = 1.0;
for (int j = 0; j < N; ++j) {
double sum = A[j][j];
for (int k = 0; k < j; ++k) {
sum -= L[j][k] * L[j][k] * D[k];
}
D[j] = sum;
if (std::abs(D[j]) < 1e-12) {
D[j] = (D[j] < 0.0 ? -1e-12 : 1e-12);
}
for (int i = j + 1; i < N; ++i) {
double s = A[i][j];
for (int k = 0; k < j; ++k) {
s -= L[i][k] * L[j][k] * D[k];
}
L[i][j] = s / D[j];
}
}
double z[N];
for (int i = 0; i < N; ++i) {
double s = b[i];
for (int k = 0; k < i; ++k) s -= L[i][k] * z[k];
z[i] = s;
}
double y[N];
for (int i = 0; i < N; ++i) y[i] = z[i] / D[i];
for (int i = N - 1; i >= 0; --i) {
double s = y[i];
for (int k = i + 1; k < N; ++k) s -= L[k][i] * x[k];
x[i] = s;
}
}
// -----------------------------------------------------------------------------
// Stage 2 : Biquad coefficient linear gain absorption
// -----------------------------------------------------------------------------
// H(z) = (b0 + b1.z^{-1} + b2.z^{-2}) / (1 + a1.z^{-1} + a2.z^{-2})
//
// frequency amplitude linearGain , (b0, b1, b2) linearGain
// . mathematically independent DC color apply completely .
BiquadCoeffs MagnitudeResponseFitter::absorbGainIntoBiquad(
const BiquadCoeffs& c, float linearGain) noexcept
{
BiquadCoeffs result = c;
result.b0 *= linearGain;
result.b1 *= linearGain;
result.b2 *= linearGain;
return result;
}
// -----------------------------------------------------------------------------
// Stage 2: Interaction Matrix before compute
// -----------------------------------------------------------------------------
void MagnitudeResponseFitter::precomputeInteractionMatrix(double sampleRate) {
if (cacheValid && std::abs(cachedSampleRate - sampleRate) < 0.5) {
return;
}
constexpr int N = NUM_BANDS;
constexpr float kProbeGainDB = 1.0f;
for (int j = 0; j < N; ++j) {
BiquadCoeffs c = designSymmetricPeakBiquad(
BAND_FREQ[j], kProbeGainDB, kBandQs[j], sampleRate);
for (int i = 0; i < N; ++i) {
float dB = biquadMagnitudeDB(c, BAND_FREQ[i], sampleRate);
cachedB[i][j] = static_cast<double>(dB);
}
}
const std::array<double, NUM_BANDS> weights = {
0.5, // 31.25 Hz
0.7, // 62.5 Hz
0.85, // 125 Hz
1.0, // 250 Hz
1.0, // 500 Hz
1.0, // 1 kHz
1.0, // 2 kHz
1.0, // 4 kHz
0.85, // 8 kHz
0.6 // 16 kHz
};
for (int i = 0; i < N; ++i) cachedW[i] = weights[i];
for (int i = 0; i < N; ++i) {
for (int j = 0; j < N; ++j) {
double s = 0.0;
for (int k = 0; k < N; ++k) {
s += cachedB[k][i] * cachedW[k] * cachedB[k][j];
}
cachedBtWB[i][j] = s;
}
}
constexpr double kRidge = 1e-4;
for (int i = 0; i < N; ++i) cachedBtWB[i][i] += kRidge;
cachedSampleRate = sampleRate;
cacheValid = true;
}
// -----------------------------------------------------------------------------
// Stage 2c: main design function ( fix )
// -----------------------------------------------------------------------------
// :
// 1. band target dB compute (T60 dB )
// t[i] = -60 . m / (fs . T60[i])
// 2. LF/HF correction target dB directly
// 3. target dB 0 below clamp -> loop gain <= 1 guarantee
// 4. mid-band gain midGain (band 4 = 500Hz)
// midGain = 10^(midDb/20)
// 5. dB WLS
// g_cmd = (B^T.W.B)^(-1).B^T.W.t_residual
// 6. g_cmd[j] dB Biquad coefficient
// 7. band 0 coefficient midGain absorption
// -> independent DC color apply not needed
MagnitudeResponseFitter::DesignResultStage2 MagnitudeResponseFitter::designStage2(
int delaySamples,
double sampleRate,
const std::array<float, NUM_BANDS>& rt60,
float hfDamping,
float lfAbsorption)
{
precomputeInteractionMatrix(sampleRate);
DesignResultStage2 result;
constexpr int N = NUM_BANDS;
const float fs = static_cast<float>(sampleRate);
const float m = static_cast<float>(delaySamples);
// -- Step 1: band loop 1 gain dB target --
std::array<float, NUM_BANDS> targetDb;
for (int i = 0; i < N; ++i) {
float t60Safe = std::max(0.01f, rt60[i]);
targetDb[i] = -60.0f * m / (fs * t60Safe);
}
// -- Step 2: LF/HF correction target dB --
// LF Absorption: low band (31Hz, 62Hz, 125Hz) added decay
// lfAbsorption=0 -> correction , =1 -> -3dB added decay
targetDb[0] += -lfAbsorption * 3.0f;
targetDb[1] += -lfAbsorption * 2.5f;
targetDb[2] += -lfAbsorption * 1.5f;
// HF Damping: high band (4kHz, 8kHz, 16kHz) added decay
// hfDamping=0 -> correction , =1 -> -6dB added decay
targetDb[7] += -hfDamping * 3.0f;
targetDb[8] += -hfDamping * 5.0f;
targetDb[9] += -hfDamping * 6.0f;
// -- Step 3: target dB 0 below clamp --
// loop gain <= 1 mathematically guarantee safe
for (int i = 0; i < N; ++i) {
targetDb[i] = std::min(targetDb[i], 0.0f);
// decay precision influence below (-60dB/loop)
targetDb[i] = std::max(targetDb[i], -60.0f);
result.targetDb[i] = targetDb[i];
}
// -- Step 4: mid-band gain midGain (band 4 = 500Hz) --
float midDb = targetDb[4];
float midGainLinear = std::pow(10.0f, midDb / 20.0f);
result.midGainAbsorbed = midGainLinear;
// dB: mid-band deviation (GEQ frequency response )
std::array<double, NUM_BANDS> residualDb;
for (int i = 0; i < N; ++i) {
residualDb[i] = static_cast<double>(targetDb[i] - midDb);
}
// -- Step 5: WLS GEQ coefficient --
std::array<double, NUM_BANDS> rhs;
for (int j = 0; j < N; ++j) {
double s = 0.0;
for (int k = 0; k < N; ++k) {
s += cachedB[k][j] * cachedW[k] * residualDb[k];
}
rhs[j] = s;
}
std::array<double, NUM_BANDS> gCmd;
solveLDLT10(cachedBtWB, rhs, gCmd);
// -- Step 6: g_cmd[j] dB Biquad coefficient --
// safe range clamp (+/-18 dB )
for (int j = 0; j < N; ++j) {
float gDb = static_cast<float>(juce::jlimit(-18.0, 18.0, gCmd[j]));
result.commandDb[j] = gDb;
result.geqStages[j] = designSymmetricPeakBiquad(
BAND_FREQ[j], gDb, kBandQs[j], sampleRate);
}
// -- Step 7: band 0 coefficient midGain absorption --
// independent DC color apply not needed ,
// filter cascade entire loop gain exact WLS .
result.geqStages[0] = absorbGainIntoBiquad(result.geqStages[0], midGainLinear);
return result;
}
} // namespace FDNReverb

View file

@ -0,0 +1,130 @@
#pragma once
#include "DSPConstants.h"
#include "BiquadFilters.h"
#include "../AlgorithmPresets.h"
#include <array>
namespace FDNReverb {
// -----------------------------------------------------------------------------
// MagnitudeResponseFitter
// -----------------------------------------------------------------------------
// designs the 10-band RT60 absorption filters for the FDN.
//
// design modes :
// Stage 1 (Jot first-order orthogonalizing):
// Jot-Chaigne (AES Preprint 3030, 1991) first-order orthogonalizing filters.
// matched at DC and Nyquist with 2 design points.
//
// Stage 2c (Valimaki-Liski cumulative GEQ):
// Valimaki & Liski (IEEE SPL 2017) Interaction Matrix + WLS
// exact fit across the 10 bands.
//
// safety guarantee :
// - targets are clamped to 0 dB or below -> loop gain <= 1 is guaranteed
// - band 0 midGain and b0/b1/b2 are absorbed into the applied filter
// - LF/HF corrections are independent GEQ targets in dB
//
// important :
// - per-band decay in dB: -60*m / (fs*T60)
// avoids the "2 kHz T60 assumption" of Schlecht-Habets (DAFx-17)
// - design runs offline (message thread); the resulting Biquad coefficients
// are used on the audio thread
// -----------------------------------------------------------------------------
class MagnitudeResponseFitter {
public:
enum class DesignMode {
Stage1_Jot1stOrder, // Jot first-order orthogonalizing (2 pts: DC/Nyquist)
Stage2_BiquadGEQ // Valimaki-Liski cumulative GEQ (exact at 10 bands)
};
// -------------------------------------------------------------------------
// Stage 1 design result (existing)
// -------------------------------------------------------------------------
// ABSO_STAGES = 3 Biquads:
// coeffs[0] = gain (Jot first-order orthogonalizing filter, Biquad form)
// coeffs[1] = low-band correction (Low Shelf, LF Absorption)
// coeffs[2] = high-band correction (High Shelf, HF Damping)
struct DesignResult {
std::array<BiquadCoeffs, ABSO_STAGES> coeffs;
float dcGain{ 1.0f };
float nyquistGain{ 1.0f };
float pole{ 0.0f };
};
// -------------------------------------------------------------------------
// Stage 2c design result
// -------------------------------------------------------------------------
// 10-band GEQ:
// geqStages[0] = band 0 (31.25 Hz), midGain absorbed into the coefficient
// geqStages[1..9] = bands 1-9 (62.5 Hz - 16 kHz), GEQ
//
// filter chain: geqStages[0] -> geqStages[1] -> ... -> geqStages[9]
// no separate midGain stage is needed (absorbed into band 0).
struct DesignResultStage2 {
std::array<BiquadCoeffs, NUM_BANDS> geqStages; // 10-band GEQ
// visualization
std::array<float, NUM_BANDS> targetDb; // per-band target dB (after clamping)
std::array<float, NUM_BANDS> commandDb; // WLS-solved command dB
float midGainAbsorbed{ 1.0f }; // midGain absorbed into band 0
};
// -------------------------------------------------------------------------
// Stage 1 design function (existing)
// -------------------------------------------------------------------------
static DesignResult design(
int delaySamples,
double sampleRate,
const std::array<float, NUM_BANDS>& rt60,
float hfDamping,
float lfAbsorption);
// -------------------------------------------------------------------------
// Stage 2c design function
// -------------------------------------------------------------------------
static DesignResultStage2 designStage2(
int delaySamples,
double sampleRate,
const std::array<float, NUM_BANDS>& rt60,
float hfDamping,
float lfAbsorption);
// -------------------------------------------------------------------------
// precompute the interaction matrix once (per sample rate)
// -------------------------------------------------------------------------
static void precomputeInteractionMatrix(double sampleRate);
static double getCachedSampleRate() noexcept { return cachedSampleRate; }
private:
// -- Stage 1 --
static float t60ToLoopGain(float t60Seconds, int delaySamples, double sampleRate) noexcept;
static float computeJotPole(float gDC, float alphaRatio) noexcept;
static BiquadCoeffs orthogonalizedFirstOrderToBiquad(float gain, float pole) noexcept;
static float getT60AtDC(const std::array<float, NUM_BANDS>& rt60) noexcept;
static float getT60AtNyquist(const std::array<float, NUM_BANDS>& rt60, double sampleRate) noexcept;
// -- Stage 2 --
static BiquadCoeffs designSymmetricPeakBiquad(
float fcHz, float gainDB, float Q, double sampleRate) noexcept;
static const std::array<float, NUM_BANDS>& getBandFreqs() noexcept { return BAND_FREQ; }
static const std::array<float, NUM_BANDS>& getBandQs() noexcept;
static float biquadMagnitudeDB(const BiquadCoeffs& c, float fEval, double sampleRate) noexcept;
static void solveLDLT10(
const std::array<std::array<double, NUM_BANDS>, NUM_BANDS>& A,
const std::array<double, NUM_BANDS>& b,
std::array<double, NUM_BANDS>& x) noexcept;
// absorb the entire DC gain of the Biquad (b0, b1, b2) into a gain
// so an independent DC gain can be applied to the filter mathematically
static BiquadCoeffs absorbGainIntoBiquad(const BiquadCoeffs& c, float linearGain) noexcept;
// -- Stage 2 static --
static std::array<std::array<double, NUM_BANDS>, NUM_BANDS> cachedB;
static std::array<std::array<double, NUM_BANDS>, NUM_BANDS> cachedBtWB;
static std::array<double, NUM_BANDS> cachedW;
static double cachedSampleRate;
static bool cacheValid;
};
} // namespace FDNReverb

148
Source/DSP/OutputEQ.h Normal file
View file

@ -0,0 +1,148 @@
#pragma once
#include <cmath>
#include <algorithm>
namespace FDNReverb {
// -----------------------------------------------------------------------------
// OutputEQ: Wet output stage Lo/Hi Cut (Linkwitz-Riley 12dB/oct)
// -----------------------------------------------------------------------------
// design rationale:
// - 1 IIR (6dB/oct) x 2 cascade = 12dB/oct
// - Linkwitz-Riley topology: 2nd-order phase alignment
// - keeps the reverb sounding musical
//
// filter equation (1 IIR):
// HPF: y[n] = R . (y[n-1] + x[n] - x[n-1])
// LPF: y[n] = (1 - R) . x[n] + R . y[n-1]
// where R = exp(-2pi.fc/fs)
//
// real-time safety :
// - no allocation at all
// - per-sample cost: HPF 8 ops + LPF 6 ops (L/R combined)
// - coefficients updated per block (no zipper noise, no SmoothedValue needed)
//
// bypass :
// - Lo Cut below 20 Hz -> HPF fully bypassed
// - Hi Cut above 20 kHz -> LPF fully bypassed
// both bypasses are per-block coefficient updates, so CPU use is trivial.
// -----------------------------------------------------------------------------
class OutputEQ {
public:
OutputEQ() = default;
void prepare(double sampleRate) noexcept {
fs = sampleRate;
reset();
setLoCutHz(20.0f);
setHiCutHz(20000.0f);
}
void reset() noexcept {
// HPF state (two stages per channel, L/R)
hpfX1_L_1 = hpfY1_L_1 = 0.0f;
hpfX1_L_2 = hpfY1_L_2 = 0.0f;
hpfX1_R_1 = hpfY1_R_1 = 0.0f;
hpfX1_R_2 = hpfY1_R_2 = 0.0f;
// LPF state (two stages per channel, L/R)
lpfY1_L_1 = 0.0f;
lpfY1_L_2 = 0.0f;
lpfY1_R_1 = 0.0f;
lpfY1_R_2 = 0.0f;
}
// --- parameter setters (called per block) ---
void setLoCutHz(float fcHz) noexcept {
currentLoCutHz = fcHz;
// bypass below 20 Hz (skip R computation)
if (fcHz <= 20.0f) {
loCutActive = false;
return;
}
loCutActive = true;
constexpr float twoPi = 6.28318530718f;
const float clamped = std::clamp(fcHz, 20.0f, 500.0f);
loCutR = std::exp(-twoPi * clamped / static_cast<float>(fs));
}
void setHiCutHz(float fcHz) noexcept {
currentHiCutHz = fcHz;
// bypass above 20 kHz
const float nyquist = static_cast<float>(fs) * 0.45f;
const float clamped = std::clamp(fcHz, 1000.0f, std::min(20000.0f, nyquist));
if (fcHz >= 20000.0f) {
hiCutActive = false;
return;
}
hiCutActive = true;
constexpr float twoPi = 6.28318530718f;
hiCutR = std::exp(-twoPi * clamped / static_cast<float>(fs));
}
// --- per-sample processing (L/R interleaved) ---
inline void process(float& l, float& r) noexcept {
// -- Lo Cut: 1 HPF x 2 cascade --
if (loCutActive) {
// L stage 1
const float l_in = l;
const float l_1 = loCutR * (hpfY1_L_1 + l_in - hpfX1_L_1);
hpfX1_L_1 = l_in;
hpfY1_L_1 = l_1;
// L stage 2
const float l_2 = loCutR * (hpfY1_L_2 + l_1 - hpfX1_L_2);
hpfX1_L_2 = l_1;
hpfY1_L_2 = l_2;
l = l_2;
// R stage 1
const float r_in = r;
const float r_1 = loCutR * (hpfY1_R_1 + r_in - hpfX1_R_1);
hpfX1_R_1 = r_in;
hpfY1_R_1 = r_1;
// R stage 2
const float r_2 = loCutR * (hpfY1_R_2 + r_1 - hpfX1_R_2);
hpfX1_R_2 = r_1;
hpfY1_R_2 = r_2;
r = r_2;
}
// -- Hi Cut: 1 LPF x 2 cascade --
if (hiCutActive) {
const float oneMinusR = 1.0f - hiCutR;
// L stage 1
lpfY1_L_1 = oneMinusR * l + hiCutR * lpfY1_L_1;
// L stage 2
lpfY1_L_2 = oneMinusR * lpfY1_L_1 + hiCutR * lpfY1_L_2;
l = lpfY1_L_2;
// R stage 1
lpfY1_R_1 = oneMinusR * r + hiCutR * lpfY1_R_1;
// R stage 2
lpfY1_R_2 = oneMinusR * lpfY1_R_1 + hiCutR * lpfY1_R_2;
r = lpfY1_R_2;
}
}
float getCurrentLoCutHz() const noexcept { return currentLoCutHz; }
float getCurrentHiCutHz() const noexcept { return currentHiCutHz; }
private:
double fs{ 48000.0 };
// -- Lo Cut (HPF) --
bool loCutActive{ false };
float loCutR{ 0.0f };
float currentLoCutHz{ 20.0f };
float hpfX1_L_1{}, hpfY1_L_1{}, hpfX1_L_2{}, hpfY1_L_2{};
float hpfX1_R_1{}, hpfY1_R_1{}, hpfX1_R_2{}, hpfY1_R_2{};
// -- Hi Cut (LPF) --
bool hiCutActive{ false };
float hiCutR{ 0.0f };
float currentHiCutHz{ 20000.0f };
float lpfY1_L_1{}, lpfY1_L_2{};
float lpfY1_R_1{}, lpfY1_R_2{};
};
} // namespace FDNReverb

View file

@ -0,0 +1,79 @@
#pragma once
#include <cmath>
#include <algorithm>
namespace FDNReverb {
// -----------------------------------------------------------------------------
// OutputLimiter: safe output stage (true-peak limiter)
// -----------------------------------------------------------------------------
// design rationale :
// - parameter values are chosen conservatively for safety
// - Threshold = -0.5 dBFS (~0.944): suppresses peaks before the DAW limiter
// - Look-ahead: introduces plugin latency
// - Attack: 0.5 ms (peak-based)
// - Release: 50 ms (prevents unnatural pumping)
//
// real-time safety :
// - allocation: once in prepare(), never in processBlock
// - per-sample gain: one comparison against targetGain, SIMD-friendly
// - floating-point math: no branches or transcendental functions
//
// - layout: output stage of UniversalEngine::processBlock()
// (after Dry/Wet mix, before the stereo output)
// -----------------------------------------------------------------------------
class OutputLimiter {
public:
OutputLimiter() = default;
// --- sample-rate dependent coefficient computation ---
void prepare(double sampleRate) noexcept {
fs = sampleRate;
// 1 path filter coefficient : y[n] = y[n-1] + coeff * (x[n] - y[n-1])
// coeff = 1 - exp(-T / tau) where T = 1/fs, tau = time constant
attackCoeff = 1.0f - std::exp(-1.0f / (static_cast<float>(fs) * 0.0005f)); // 0.5ms
releaseCoeff = 1.0f - std::exp(-1.0f / (static_cast<float>(fs) * 0.050f)); // 50ms
reset();
}
void reset() noexcept {
currentGain = 1.0f;
}
// --- per-sample processing (called from the audio thread) ---
inline void process(float& l, float& r) noexcept {
// peak detection (max of L/R levels)
const float absL = std::abs(l);
const float absR = std::abs(r);
const float peak = std::max(absL, absR);
// Threshold: -0.5 dBFS ~ 0.944
// compute target gain from the signal
constexpr float threshold = 0.944f;
// target gain :
// peak <= threshold -> 1.0 (no reduction needed)
// peak > threshold -> threshold/peak (pull signal to threshold)
const float targetGain = (peak > threshold) ? (threshold / peak) : 1.0f;
// Attack/Release envelope
// when targetGain < currentGain (gain must decrease): attack
// when targetGain > currentGain (gain recovers): release
// so peaks are suppressed smoothly
const float coeff = (targetGain < currentGain) ? attackCoeff : releaseCoeff;
currentGain += (targetGain - currentGain) * coeff;
// apply the same gain to L/R to preserve the stereo image
l *= currentGain;
r *= currentGain;
}
private:
double fs{ 44100.0 };
float attackCoeff{ 0.0f };
float releaseCoeff{ 0.0f };
float currentGain{ 1.0f };
};
} // namespace FDNReverb

21
Source/DSP/SAPFStage.cpp Normal file
View file

@ -0,0 +1,21 @@
#include "SAPFStage.h"
namespace FDNReverb {
void SAPFStage::prepare(const juce::dsp::ProcessSpec& spec, int delayTargetSamples) {
M = delayTargetSamples;
dl.prepare(spec);
dl.setMaximumDelayInSamples(M + 4);
dl.setDelay(static_cast<float>(M));
}
float SAPFStage::tick(float x) noexcept {
float d = dl.popSample(0);
float w = x + gain * d;
dl.pushSample(0, w);
return d - gain * w;
}
void SAPFStage::reset() noexcept { dl.reset(); }
} // namespace FDNReverb

19
Source/DSP/SAPFStage.h Normal file
View file

@ -0,0 +1,19 @@
#pragma once
#include <JuceHeader.h>
namespace FDNReverb {
class SAPFStage {
public:
void prepare(const juce::dsp::ProcessSpec& spec, int delayTargetSamples);
void setGain(float g) noexcept { gain = juce::jlimit(0.3f, 0.72f, g); }
float tick(float x) noexcept;
void reset() noexcept;
private:
juce::dsp::DelayLine<float, juce::dsp::DelayLineInterpolationTypes::Thiran> dl;
float gain{ 0.618f };
int M{ 0 };
};
} // namespace FDNReverb

197
Source/DSP/Saturator.h Normal file
View file

@ -0,0 +1,197 @@
#pragma once
#include <cmath>
#include <algorithm>
namespace FDNReverb {
enum class SaturationMode {
Warm = 0,
Tape = 1,
Tube = 2,
Hard = 3
};
class Saturator {
public:
Saturator() = default;
void reset() noexcept {
prevInput = 0.0f;
switch (currentMode) {
case SaturationMode::Warm: prevF = 1.0f; break;
case SaturationMode::Tape: prevF = 0.0f; break;
case SaturationMode::Tube: prevF = 1.0f; break;
case SaturationMode::Hard: prevF = 0.0f; break;
}
}
void setMode(SaturationMode mode) noexcept {
if (mode != currentMode) {
currentMode = mode;
reset();
}
}
void setMode(int modeIndex) noexcept {
setMode(static_cast<SaturationMode>(std::clamp(modeIndex, 0, 3)));
}
// -------------------------------------------------------------------------
// * Step B fix: only the drive curve changed; ADAA structure fully preserved
// -------------------------------------------------------------------------
// drive = 1 + amount^3 x 1.0 ( maximum 2.0) -> 1 + amount^2 x 2.5 ( maximum 3.5)
//
// amount | old drive | new drive | effect
// -------|----------|----------|--------------------
// 0.30 | 1.027 | 1.225 | + about 7.5dB stronger
// 0.50 | 1.125 | 1.625 | + about 3.2dB stronger
// 0.70 | 1.343 | 2.225 | + about 4.4dB stronger
// 1.00 | 2.000 | 3.500 | + about 4.9dB stronger
//
// -> plugin 24 harmonics visualization
// -------------------------------------------------------------------------
void setAmount(float amount) noexcept {
amount = std::clamp(amount, 0.0f, 1.0f);
currentAmount = amount;
// * Step B: amount^2 x 2.5 stronger
drive = 1.0f + amount * amount * 2.5f;
wetMix = amount * amount * 0.7f;
dryMix = 1.0f - amount * 0.25f;
}
inline float processSample(float input) noexcept {
if (currentAmount < 1e-4f) return input;
const float dryInput = input;
const float driven = input * drive;
float saturated = 0.0f;
switch (currentMode) {
case SaturationMode::Warm: saturated = processWarm(driven); break;
case SaturationMode::Tape: saturated = processTape(driven); break;
case SaturationMode::Tube: saturated = processTube(driven); break;
case SaturationMode::Hard: saturated = processHard(driven); break;
}
saturated /= drive;
return dryInput * dryMix + saturated * wetMix;
}
private:
// --- Warm: Vicanek x/sqrt(1+x^2) + ADAA 1 ---
inline float processWarm(float x) noexcept {
const float F_x = std::sqrt(1.0f + x * x);
const float dx = x - prevInput;
float y;
constexpr float kTol = 1e-5f;
if (std::abs(dx) < kTol) {
const float xAvg = (x + prevInput) * 0.5f;
y = xAvg / std::sqrt(1.0f + xAvg * xAvg);
}
else {
y = (F_x - prevF) / dx;
}
prevInput = x;
prevF = F_x;
return y;
}
// --- Tape: Pade x(27+x^2)/(27+9x^2) (ADAA intentional ) ---
inline float processTape(float x) noexcept {
if (x > 3.0f) { prevInput = x; return 1.0f; }
if (x < -3.0f) { prevInput = x; return -1.0f; }
const float xsq = x * x;
prevInput = x;
return x * (27.0f + xsq) / (27.0f + 9.0f * xsq);
}
// -------------------------------------------------------------------------
// Tube: asymmetric ADAA + * Step B: kNeg 1.5 -> 2.0
// -------------------------------------------------------------------------
// positive side : f(x) = x/sqrt(1+x^2) F(x) = sqrt(1+x^2)
// negative side : f(x) = x/sqrt(1+(kNeg.x)^2) F(x) = (1/kNeg^2)sqrt(1+(kNeg.x)^2) + fShift
//
// C^1 : x=0 F_pos(0) = F_neg(0) = 1 fShift design
// F_pos(0) = sqrt1 = 1
// F_neg(0) = (1/kNeg^2).sqrt1 + fShift = 1
// -> fShift = 1 - 1/kNeg^2
//
// kNeg=2.0 case : fShift = 1 - 0.25 = 0.75
//
// kNeg stronger effect :
// 2 -> waveform asymmetric
// -> harmonics (2f, 4f) plugin visualization
// -------------------------------------------------------------------------
inline float processTube(float x) noexcept {
// * Step B: kNeg = 1.5f -> 2.0f
constexpr float kNeg = 2.0f;
constexpr float kNeg2 = kNeg * kNeg; // 4.0f
constexpr float invKneg2 = 1.0f / kNeg2; // 0.25f
constexpr float fShift = 1.0f - invKneg2; // 0.75f
float F_x;
if (x >= 0.0f) {
F_x = std::sqrt(1.0f + x * x);
}
else {
const float kx = kNeg * x;
F_x = invKneg2 * std::sqrt(1.0f + kx * kx) + fShift;
}
const float dx = x - prevInput;
const bool signChanged = (x >= 0.0f) != (prevInput >= 0.0f);
float y;
constexpr float kTol = 1e-5f;
if (std::abs(dx) < kTol || signChanged) {
// input -> directly
if (x >= 0.0f) {
y = x / std::sqrt(1.0f + x * x);
}
else {
const float kx = kNeg * x;
y = x / std::sqrt(1.0f + kx * kx);
}
}
else {
y = (F_x - prevF) / dx;
}
prevInput = x;
prevF = F_x;
return y;
}
// --- Hard: clipping + ADAA 1 ---
inline float processHard(float x) noexcept {
float F_x;
if (x > 1.0f) F_x = x - 0.5f;
else if (x < -1.0f) F_x = -x - 0.5f;
else F_x = x * x * 0.5f;
const float dx = x - prevInput;
float y;
constexpr float kTol = 1e-5f;
if (std::abs(dx) < kTol) {
y = std::clamp(x, -1.0f, 1.0f);
}
else {
y = (F_x - prevF) / dx;
}
prevInput = x;
prevF = F_x;
return y;
}
float prevInput{ 0.0f };
float prevF{ 1.0f };
SaturationMode currentMode{ SaturationMode::Warm };
float currentAmount{ 0.0f };
float drive{ 1.0f };
float wetMix{ 0.0f };
float dryMix{ 1.0f };
};
} // namespace FDNReverb

View file

@ -0,0 +1,663 @@
#include "UniversalEngine.h"
namespace FDNReverb {
namespace {
static bool isMathPrime(int n) noexcept {
if (n < 2) return false;
if (n == 2) return true;
if (n % 2 == 0) return false;
for (int i = 3; i * i <= n; i += 2)
if (n % i == 0) return false;
return true;
}
static int findNearestUniquePrime(int target,
const std::array<int, 16>& usedPrimes,
int usedCount) noexcept {
target = std::max(target, 2);
for (int offset = 0; offset < 100000; ++offset) {
int hi = target + offset;
if (isMathPrime(hi)) {
bool used = false;
for (int k = 0; k < usedCount; ++k)
if (usedPrimes[k] == hi) { used = true; break; }
if (!used) return hi;
}
int lo = target - offset;
if (offset > 0 && lo >= 2 && isMathPrime(lo)) {
bool used = false;
for (int k = 0; k < usedCount; ++k)
if (usedPrimes[k] == lo) { used = true; break; }
if (!used) return lo;
}
}
return target;
}
} // anonymous namespace
UniversalEngine::UniversalEngine() {
fbVec.fill(0.0f);
constexpr float phi = 1.6180339887f;
for (int i = 0; i < FDN_ORDER; ++i) {
lfos[i].state = 12345u + static_cast<uint32_t>(i) * 9876u;
lfos[i].smoothed = 0.0f;
const float angle = static_cast<float>(i) * phi;
const float frac = angle - std::floor(angle);
lfos[i].rateMultiplier = 0.80f + frac * 0.40f;
// * LFO: noise LFO offset
const float cAngle = static_cast<float>(i + 5) * phi;
chorusLFOs[i].phase = cAngle - std::floor(cAngle);
const float cRateAngle = static_cast<float>(i + 11) * phi;
chorusLFOs[i].rateScale = 0.30f + (cRateAngle - std::floor(cRateAngle)) * 0.50f;
}
}
void UniversalEngine::prepare(double sampleRate, int /*maxBlockSize*/) {
fs = sampleRate;
#if AMBIVALENCE_USE_STAGE2_ABSORPTION
MagnitudeResponseFitter::precomputeInteractionMatrix(sampleRate);
#endif
auto getPow2 = [](size_t s) -> size_t {
size_t p = 1;
while (p < s) p *= 2;
return p;
};
size_t totalMemoryNeeded =
getPow2(static_cast<size_t>(fs * 0.5)) // * preDelay (max 500ms)
+ getPow2(static_cast<size_t>(fs * 1.0))
+ getPow2(static_cast<size_t>(fs * 0.05)) * 4
+ getPow2(static_cast<size_t>(fs * 0.5)) * FDN_ORDER
+ getPow2(static_cast<size_t>(fs * 0.05)) * FDN_ORDER * SERIAL_APF_STAGES;
memoryPool.allocate(totalMemoryNeeded);
int mask = 0;
float* ptr = nullptr;
// * PreDelay (max 500ms)
ptr = memoryPool.requestMemory(static_cast<size_t>(fs * 0.5), mask);
preDelayLine.init(ptr, mask);
ptr = memoryPool.requestMemory(static_cast<size_t>(fs * 1.0), mask);
erDelay.init(ptr, mask);
for (int i = 0; i < 4; ++i) {
ptr = memoryPool.requestMemory(static_cast<size_t>(fs * 0.05), mask);
inputDiffusers[i].init(ptr, mask);
}
for (int i = 0; i < FDN_ORDER; ++i) {
ptr = memoryPool.requestMemory(static_cast<size_t>(fs * 0.5), mask);
fdnDelays[i].init(ptr, mask);
for (int s = 0; s < SERIAL_APF_STAGES; ++s) {
ptr = memoryPool.requestMemory(static_cast<size_t>(fs * 0.05), mask);
nestedAllpassDelays[i][s].init(ptr, mask);
}
}
acousticMetrics.prepare(sampleRate, 2000.0f);
currentERTapCount = 0;
currentERDelaySamples.fill(0.0f);
currentERGains.fill(0.0f);
outputLimiter.prepare(sampleRate);
outputEQ.prepare(sampleRate);
duckingAttackCoeff = 1.0f - std::exp(-1.0f / (static_cast<float>(fs) * 0.010f));
duckingReleaseCoeff = 1.0f - std::exp(-1.0f / (static_cast<float>(fs) * 0.200f));
duckingEnvelope = 0.0f;
// * DC coefficient : fc ~ 5Hz 1HPF
dcBlockerCoeff = 1.0f - (6.28318530718f * 5.0f / static_cast<float>(fs));
dcX1.fill(0.0f);
dcY1.fill(0.0f);
// * Soft-knee: RMS envelope coefficient (~3ms)
fdnRmsEnv.fill(0.0f);
rmsCoeff = 1.0f - std::exp(-1.0f / (static_cast<float>(fs) * 0.003f));
reset();
}
void UniversalEngine::reset() {
memoryPool.clear();
fbVec.fill(0.0f);
#if AMBIVALENCE_USE_STAGE2_ABSORPTION
for (auto& lineFilters : absorptionFiltersS2)
for (auto& f : lineFilters) f.reset();
#else
for (auto& f : absorptionFilters) f.reset();
#endif
acousticMetrics.reset();
saturatorL.reset();
saturatorR.reset();
outputLimiter.reset();
outputEQ.reset();
duckingEnvelope = 0.0f;
dcX1.fill(0.0f);
dcY1.fill(0.0f);
fdnRmsEnv.fill(0.0f);
for (auto& dl : fdnDelays) dl.resetState(); // * Thiran allpass state
for (auto& lfo : lfos) lfo.smoothed = 0.0f;
}
void UniversalEngine::setParams(const DSPParams& p) {
activeParams = p;
switch (p.algorithmIndex) {
case 0: case 1: currentTopology = ReverbTopology::Room; break;
case 2: case 3: currentTopology = ReverbTopology::Hall; break;
case 4: currentTopology = ReverbTopology::Plate; break;
case 5: currentTopology = ReverbTopology::Spring; break;
case 6: currentTopology = ReverbTopology::Goldfoil; break;
}
const float attMs = juce::jmax(0.1f, p.duckingAttackMs);
const float relMs = juce::jmax(0.1f, p.duckingRelMs);
duckingAttackCoeff = 1.0f - std::exp(-1.0f / (static_cast<float>(fs) * attMs * 0.001f));
duckingReleaseCoeff = 1.0f - std::exp(-1.0f / (static_cast<float>(fs) * relMs * 0.001f));
// * PreDelay: ms -> sample count
preDelaySamples = p.preDelayMs * 0.001f * static_cast<float>(fs);
outputEQ.setLoCutHz(p.loCutHz);
outputEQ.setHiCutHz(p.hiCutHz);
updateTopologyAndRouting();
}
void UniversalEngine::calculatePrimePowerDelays() {
const float fsf = static_cast<float>(fs);
const float sizeCoeff = juce::jlimit(0.5f, 2.0f, activeParams.roomSizeScale + 1.0f);
const float minDelayMs = 15.0f + sizeCoeff * 7.5f;
const float maxDelayMs = 50.0f + sizeCoeff * 75.0f;
const int minDelaySamples = std::max(11, static_cast<int>(minDelayMs * 0.001f * fsf));
const int maxDelaySamples = static_cast<int>(maxDelayMs * 0.001f * fsf);
const float logMin = std::log(static_cast<float>(minDelaySamples));
const float logMax = std::log(static_cast<float>(maxDelaySamples));
std::array<int, FDN_ORDER> usedPrimes;
usedPrimes.fill(0);
for (int i = 0; i < FDN_ORDER; ++i) {
const float t = static_cast<float>(i) / static_cast<float>(FDN_ORDER - 1);
const float logTgt = logMin + t * (logMax - logMin);
const int target = static_cast<int>(std::round(std::exp(logTgt)));
const int prime = findNearestUniquePrime(target, usedPrimes, i);
usedPrimes[i] = prime;
fdnBaseDelaySamples[i] = static_cast<float>(prime);
}
}
void UniversalEngine::updateTopologyAndRouting() {
calculatePrimePowerDelays();
auto& preset = *ALL_PRESETS[activeParams.algorithmIndex];
std::array<float, NUM_BANDS> scaledRT60 = preset.acoustics.rt60;
for (auto& v : scaledRT60) v *= activeParams.decayScale;
// -------------------------------------------------------------------------
// * 2) fix : proMode always Tilt / band apply
// -------------------------------------------------------------------------
// old implementation : if (activeParams.proMode) { ... }
// when ProMode is OFF, the Tilt / band coefficients were not applied,
// so the RT60 graph kept the preset's original curve.
//
// new implementation: always apply; the coefficients default to 1.0f,
// so changing them scales the RT60 graph,
// and reset to 1.0f when loadPresetDefaults() is called.
//
// -------------------------------------------------------------------------
scaledRT60[0] *= activeParams.tiltLow;
scaledRT60[1] *= activeParams.tiltLow;
scaledRT60[2] *= activeParams.tiltLow;
scaledRT60[3] *= activeParams.tiltMid;
scaledRT60[4] *= activeParams.tiltMid;
scaledRT60[5] *= activeParams.tiltMid;
scaledRT60[6] *= activeParams.tiltMid;
scaledRT60[7] *= activeParams.tiltHigh;
scaledRT60[8] *= activeParams.tiltHigh;
scaledRT60[9] *= activeParams.tiltHigh;
for (int b = 0; b < NUM_BANDS; ++b)
scaledRT60[b] *= activeParams.rtBands[b];
#if AMBIVALENCE_USE_STAGE2_ABSORPTION
std::array<float, NUM_BANDS> targetDbAccum;
targetDbAccum.fill(0.0f);
for (int i = 0; i < FDN_ORDER; ++i) {
auto s2 = MagnitudeResponseFitter::designStage2(
static_cast<int>(fdnBaseDelaySamples[i]), fs, scaledRT60,
activeParams.hfDamping, activeParams.lfAbsorption);
for (int b = 0; b < NUM_BANDS; ++b) {
currentAbsorptionCoeffsS2[i][b] = s2.geqStages[b];
targetDbAccum[b] += s2.targetDb[b];
}
}
const float representativeDelay = fdnBaseDelaySamples[FDN_ORDER / 2];
for (int b = 0; b < NUM_BANDS; ++b) {
const float avgTargetDb = targetDbAccum[b] / static_cast<float>(FDN_ORDER);
if (avgTargetDb < -0.001f) {
effectiveRT60[b] = -60.0f * representativeDelay
/ (static_cast<float>(fs) * avgTargetDb);
}
else {
effectiveRT60[b] = scaledRT60[b];
}
effectiveRT60[b] = juce::jlimit(0.05f, 30.0f, effectiveRT60[b]);
}
#else
effectiveRT60 = scaledRT60;
for (int i = 0; i < FDN_ORDER; ++i) {
auto absoStages = FilterDesign::designAbsorption(
static_cast<int>(fdnBaseDelaySamples[i]), fs, scaledRT60,
activeParams.hfDamping, activeParams.lfAbsorption);
currentAbsorptionCoeffs[i] = absoStages[0];
}
#endif
// -------------------------------------------------------------------------
// * EDT fix : band average LF/HF correction
// -------------------------------------------------------------------------
// old implementation : effectiveRT60[4] (500Hz) band use
// -> HF Damping high band below EDT
// -> LF Absorption low band below EDT
//
// new implementation : mid-band band (125Hz~4kHz = band 2~7) average value use
// -> band LF/HF correction influence
// -> (31Hz, 63Hz, 8kHz, 16kHz) ( psychoacoustically EDT
// , value unstable )
// -------------------------------------------------------------------------
float rt60Mid = 0.0f;
for (int b = 2; b <= 7; ++b)
rt60Mid += effectiveRT60[b];
rt60Mid = std::max(0.1f, rt60Mid / 6.0f);
// -------------------------------------------------------------------------
// * metallic sound (1): Decay depends on saturation
// -------------------------------------------------------------------------
// each FDN loop pass runs processMicroSaturation(), and reverberation
// nonlinear distortion accumulates in the reverb and shifts the filter
// response, producing metallic ringing.
//
// policy: not applied below a 2.0 s mid-band RT60 average, scaled between 2.0 s and 6.0 s,
// and fully bypassed above 6.0 s.
// -------------------------------------------------------------------------
microSatBlend = juce::jlimit(0.0f, 1.0f, 1.0f - (rt60Mid - 2.0f) / 4.0f);
// -------------------------------------------------------------------------
// * metallic sound (2): Decay depends on modulation
// -------------------------------------------------------------------------
// longer reverb tails require deeper modulation at the filter peaks.
// as used by Lexicon / Strymon.
//
// * modulation depth (scaled down for short reverbs)
// RT60 <= 1.0 s -> 1.0x (min)
// RT60 = 3.0s -> 2.0x
// RT60 >= 5.0 s -> 3.0x (max)
// -------------------------------------------------------------------------
modDepthScale = 1.0f + juce::jlimit(0.0f, 2.0f, (rt60Mid - 1.0f) * 0.5f);
constexpr float baseDB = 16.0f;
float decayCompDB = 7.0f * std::log10(rt60Mid);
static constexpr std::array<float, 7> algorithmOffsetDB = {
+0.8f, +0.9f, +0.5f, +0.5f, +1.5f, +0.6f, +0.6f
};
float algoOffset = algorithmOffsetDB[juce::jlimit(0, 6, activeParams.algorithmIndex)];
switch (currentTopology) {
case ReverbTopology::Room:
bypassER = false; bypassInputDiffusers = false;
apfGain = 0.3f; diffusionSensitivity = 1.0f;
break;
case ReverbTopology::Hall:
bypassER = false; bypassInputDiffusers = false;
apfGain = 0.618f; diffusionSensitivity = 1.0f;
break;
case ReverbTopology::Plate:
bypassER = true; bypassInputDiffusers = false;
apfGain = 0.7f; diffusionSensitivity = 0.7f;
break;
case ReverbTopology::Spring:
bypassER = true; bypassInputDiffusers = false;
apfGain = 0.5f; diffusionSensitivity = 0.5f;
break;
case ReverbTopology::Goldfoil:
bypassER = true; bypassInputDiffusers = false;
apfGain = 0.75f; diffusionSensitivity = 0.8f;
break;
}
const auto& erPattern = PRESET_ER_PATTERNS[
juce::jlimit(0, 6, activeParams.algorithmIndex)];
currentERTapCount = erPattern.numTaps;
float erSizeScale = 0.5f + activeParams.roomSizeScale;
for (int i = 0; i < erPattern.numTaps; ++i) {
currentERDelaySamples[i] = erPattern.taps[i].delayMs * 0.001f
* static_cast<float>(fs) * erSizeScale;
currentERGains[i] = erPattern.taps[i].gain;
}
if (erPattern.numTaps == 0) bypassER = true;
float edtCoeff = 0.7f;
switch (currentTopology) {
case ReverbTopology::Room: edtCoeff = 0.70f; break;
case ReverbTopology::Hall: edtCoeff = 0.95f; break;
case ReverbTopology::Plate: edtCoeff = 0.60f; break;
case ReverbTopology::Spring: edtCoeff = 0.50f; break;
case ReverbTopology::Goldfoil: edtCoeff = 0.85f; break;
}
theoreticalEDT = rt60Mid * edtCoeff;
float satMultiplier = 1.0f;
switch (currentTopology) {
case ReverbTopology::Room: satMultiplier = 0.90f; break;
case ReverbTopology::Hall: satMultiplier = 0.93f; break;
case ReverbTopology::Plate: satMultiplier = 1.00f; break;
case ReverbTopology::Spring: satMultiplier = 1.05f; break;
case ReverbTopology::Goldfoil: satMultiplier = 1.02f; break;
}
float effectiveSatAmount = juce::jlimit(0.0f, 1.0f,
activeParams.saturation * satMultiplier);
saturatorL.setAmount(effectiveSatAmount);
saturatorR.setAmount(effectiveSatAmount);
saturatorL.setMode(activeParams.satTypeIdx);
saturatorR.setMode(activeParams.satTypeIdx);
lateMakeupGainLinear = juce::Decibels::decibelsToGain(baseDB + decayCompDB + algoOffset);
}
inline void UniversalEngine::fastWalshHadamardTransform(
std::array<float, 16>& v) noexcept
{
for (int h = 1; h < 16; h *= 2) {
for (int i = 0; i < 16; i += h * 2) {
for (int j = i; j < i + h; ++j) {
float x = v[j], y = v[j + h];
v[j] = x + y;
v[j + h] = x - y;
}
}
}
for (int i = 0; i < 16; ++i) v[i] *= 0.25f;
}
inline void UniversalEngine::applySignFlipping(
std::array<float, 16>& v) noexcept
{
static constexpr std::array<float, 16> flip = {
1.f, -1.f, 1.f, -1.f, -1.f, 1.f, -1.f, 1.f,
1.f, 1.f, -1.f, -1.f, -1.f, -1.f, 1.f, 1.f
};
for (int i = 0; i < 16; ++i) v[i] *= flip[i];
}
void UniversalEngine::processBlock(const float* inL, const float* inR,
float* outL, float* outR,
int numSamples) noexcept
{
// * CPU: fs float (processBlock throughout use )
const float fsf = static_cast<float>(fs);
// * modulation : squared curve + coefficient suppress
// modAmount^2 low band gradually , 0.001f entire
// : modAmt=0.5 -> 48smp(1ms) / : modAmt=0.5 -> 12smp(0.25ms)
const float modAmtCurved = activeParams.modAmount * activeParams.modAmount;
const float depthSamples = modAmtCurved * 0.001f * fsf * modDepthScale;
const float wetGain = juce::Decibels::decibelsToGain(activeParams.wetDB);
const float stereoWidth = activeParams.stereoWidth;
const float erLevel = activeParams.erLevel;
const float lateLevel = activeParams.lateLevel;
const bool erSolo = activeParams.erSolo;
const float duckThreshLin = juce::Decibels::decibelsToGain(activeParams.duckingThreshDB);
const float duckAmountDB = activeParams.duckingAmount;
const float effectiveDiffusion = activeParams.diffusion * diffusionSensitivity;
const float diffuserGain = 0.25f + effectiveDiffusion * 0.55f;
const float effectiveApfGain = apfGain * (0.60f + effectiveDiffusion * 0.40f);
const float sideBoost = stereoWidth * 1.5f;
const float erLeakage = (1.0f - stereoWidth) * 0.7f;
// * CPU: apfGainStage loop -> before compute
const float apfGainStage = effectiveApfGain * 0.78f;
// * CPU: freqModScale before compute (16ch)
std::array<float, FDN_ORDER> freqModScales;
constexpr float invFdnM1 = 1.0f / static_cast<float>(FDN_ORDER - 1);
for (int i = 0; i < FDN_ORDER; ++i)
freqModScales[i] = 0.5f + (1.0f - static_cast<float>(i) * invFdnM1) * 1.0f;
// * CPU: input diffuser time before compute
std::array<float, 4> diffuserDelaySmp;
for (int i = 0; i < 4; ++i)
diffuserDelaySmp[i] = (3.0f + i * 2.0f) * 0.001f * fsf;
// * CPU: Allpass before compute (16ch x 3)
constexpr float apfBaseMs[SERIAL_APF_STAGES] = { 1.5f, 2.3f, 3.7f };
constexpr float apfSpreadMs[SERIAL_APF_STAGES] = { 0.30f, 0.37f, 0.47f };
constexpr float apfModFrac[SERIAL_APF_STAGES] = { 0.15f, 0.10f, 0.07f };
const float msToSmp = 0.001f * fsf;
std::array<std::array<float, SERIAL_APF_STAGES>, FDN_ORDER> apfBaseDelaySmp;
for (int i = 0; i < FDN_ORDER; ++i)
for (int s = 0; s < SERIAL_APF_STAGES; ++s)
apfBaseDelaySmp[i][s] = (apfBaseMs[s] + i * apfSpreadMs[s]) * msToSmp;
// * CPU: ER tapGain * 0.5f before compute
std::array<float, MAX_ER_TAPS> erTapGainsHalf;
for (int t = 0; t < currentERTapCount; ++t)
erTapGainsHalf[t] = currentERGains[t] * 0.5f;
// * CPU: soft-knee threshold squared before compute (sqrt avoid )
constexpr float compThresh = 0.35f;
constexpr float compThreshSq = compThresh * compThresh;
std::array<float, FDN_ORDER> lfoCoeffs;
{
constexpr float twoPi = 6.28318530718f;
for (int i = 0; i < FDN_ORDER; ++i) {
const float fc = activeParams.modRate * lfos[i].rateMultiplier;
lfoCoeffs[i] = juce::jlimit(0.0001f, 0.9999f,
1.0f - std::exp(-twoPi * fc / fsf));
// * LFO update
chorusLFOs[i].phaseInc = activeParams.modRate * chorusLFOs[i].rateScale / fsf;
}
}
for (int n = 0; n < numSamples; ++n) {
const float leftIn = inL[n];
const float rightIn = inR[n];
const float midIn = (leftIn + rightIn) * 0.5f;
const float sideIn = (leftIn - rightIn) * 0.5f;
float erOutL = 0.0f, erOutR = 0.0f;
// * PreDelay: dry time
// ERFDN input .
// dry attack after ,
// clarity (D50/C50) significantly above .
preDelayLine.write(midIn);
const float delayedMid = (preDelaySamples > 0.5f)
? preDelayLine.read(preDelaySamples)
: midIn;
const float inputPeak = juce::jmax(std::abs(leftIn), std::abs(rightIn));
const float envCoeff = (inputPeak > duckingEnvelope)
? duckingAttackCoeff : duckingReleaseCoeff;
duckingEnvelope += (inputPeak - duckingEnvelope) * envCoeff;
float duckGainLinear = 1.0f;
if (duckAmountDB > 0.001f && duckingEnvelope > duckThreshLin) {
const float envDB = 20.0f * std::log10(juce::jmax(duckingEnvelope, 1e-6f));
const float overDB = envDB - activeParams.duckingThreshDB;
const float gainRedDB = -juce::jmin(overDB, duckAmountDB);
duckGainLinear = juce::Decibels::decibelsToGain(gainRedDB);
}
float fdnInputMid = delayedMid;
if (!bypassInputDiffusers) {
for (int i = 0; i < 4; ++i) {
float d = inputDiffusers[i].read(diffuserDelaySmp[i]);
float w = fdnInputMid + diffuserGain * d;
inputDiffusers[i].write(w);
fdnInputMid = d - diffuserGain * w;
}
}
if (!bypassER) {
erDelay.write(delayedMid);
float erTotalL = 0.0f, erTotalR = 0.0f;
for (int t = 0; t < currentERTapCount; ++t) {
const float tapValue = erDelay.read(currentERDelaySamples[t]);
const float tapGain = erTapGainsHalf[t];
const float tg = tapValue * tapGain;
const float tgLeak = tg * erLeakage;
if (t % 2 == 0) {
erTotalL += tg;
erTotalR += tgLeak;
}
else {
erTotalR += tg;
erTotalL += tgLeak;
}
}
erOutL = erTotalL;
erOutR = erTotalR;
}
// * ER -> Late: feed the ER output into the FDN input
// the early reflections are wall-surface reflections that seed the Late Reverb,
// making the ER-to-Late transition natural and smooth.
if (!bypassER) {
fdnInputMid += (erOutL + erOutR) * 0.5f * 0.15f;
}
std::array<float, 16> currentFb = fbVec;
fastWalshHadamardTransform(currentFb);
applySignFlipping(currentFb);
float fdnOutL = 0.0f, fdnOutR = 0.0f;
std::array<float, 16> nextFb;
for (int i = 0; i < FDN_ORDER; ++i) {
const float lfoVal = lfos[i].tick(lfoCoeffs[i]);
// * modulation: sine-wave LFO + noise LFO
// noise = random (suppresses metallic ringing)
// chorus = smoothly accumulated (rich tail)
const float chorusVal = chorusLFOs[i].tick();
const float combinedLfo = lfoVal + chorusVal * 0.6f;
// * frequency-dependent modulation: high bands modulate less than low bands
const float freqModScale = freqModScales[i];
const float delaySmp = fdnBaseDelaySamples[i]
+ combinedLfo * depthSamples * freqModScale;
float d = fdnDelays[i].read(delaySmp);
#if AMBIVALENCE_USE_STAGE2_ABSORPTION
for (int s = 0; s < ABSO_STAGES_S2; ++s)
d = absorptionFiltersS2[i][s].tick(d, currentAbsorptionCoeffsS2[i][s]);
#else
d = absorptionFilters[i].tick(d, currentAbsorptionCoeffs[i]);
#endif
// * metallic sound (3): DC blocker (1st-order HPF, fc ~ 5 Hz)
// saturation in the FDN loop absorption filters can
// accumulate DC; blocking it prevents low-band asymmetric distortion.
{
const float dcIn = d;
const float dcOut = dcIn - dcX1[i] + dcBlockerCoeff * dcY1[i];
dcX1[i] = dcIn;
dcY1[i] = dcOut;
d = dcOut;
}
// * soft-knee compression (in the FDN feedback loop)
// an RMS envelope over the threshold triggers compression.
// * CPU: sqrt only runs above threshold (compare on squared values)
{
fdnRmsEnv[i] += (d * d - fdnRmsEnv[i]) * rmsCoeff;
if (fdnRmsEnv[i] > compThreshSq) {
const float env = std::sqrt(fdnRmsEnv[i]);
const float over = env - compThresh;
d *= compThresh / (compThresh + over * 0.65f);
}
}
// * metallic sound (1): Decay depends on saturation
// microSatBlend=1.0 -> applied (into the reverb loop)
// microSatBlend=0.0 -> fully bypassed
if (microSatBlend > 0.001f) {
const float sat = processMicroSaturation(d);
d = d + (sat - d) * microSatBlend;
}
// * 3 nested allpass filters (echo density)
// * CPU: apfGainStage precomputed per block
float apfOut = d;
{
for (int s = 0; s < SERIAL_APF_STAGES; ++s) {
const float apfModDepth = depthSamples * apfModFrac[s];
const float apfDelaySmp = apfBaseDelaySmp[i][s]
+ combinedLfo * apfModDepth * freqModScale;
float apfD = nestedAllpassDelays[i][s].read(apfDelaySmp);
float apfW = apfOut + apfGainStage * apfD;
nestedAllpassDelays[i][s].write(apfW);
apfOut = apfD - apfGainStage * apfW;
}
}
nextFb[i] = apfOut;
const float sideForCh = (i % 2 == 0 ? +sideIn : -sideIn) * sideBoost;
const float fdnInputForThisCh = (fdnInputMid + sideForCh) * 0.25f;
fdnDelays[i].write(fdnInputForThisCh + currentFb[i]);
const float crossLeak = 1.0f - stereoWidth;
if (i % 2 == 0) {
fdnOutL += apfOut;
fdnOutR += apfOut * crossLeak;
}
else {
fdnOutR += apfOut;
fdnOutL += apfOut * crossLeak;
}
}
fdnOutL *= 0.125f;
fdnOutR *= 0.125f;
fbVec = nextFb;
const float erMixL = bypassER ? 0.0f : erOutL * erLevel;
const float erMixR = bypassER ? 0.0f : erOutR * erLevel;
const float lateMixL = fdnOutL * lateMakeupGainLinear * lateLevel;
const float lateMixR = fdnOutR * lateMakeupGainLinear * lateLevel;
acousticMetrics.processSample((lateMixL + lateMixR) * 0.5f);
float satL = saturatorL.processSample(lateMixL);
float satR = saturatorR.processSample(lateMixR);
if (erSolo) { satL = 0.0f; satR = 0.0f; }
float wetL = erMixL + satL;
float wetR = erMixR + satR;
outputEQ.process(wetL, wetR);
const float finalWetGain = wetGain * duckGainLinear;
outL[n] = wetL * finalWetGain;
outR[n] = wetR * finalWetGain;
outputLimiter.process(outL[n], outR[n]);
}
}
} // namespace FDNReverb

View file

@ -0,0 +1,174 @@
#pragma once
#include "DelayMemory.h"
#include "BiquadFilters.h"
#include "MagnitudeResponseFitter.h"
#include "AcousticMetrics.h"
#include "Saturator.h"
#include "OutputLimiter.h"
#include "OutputEQ.h"
#include "../PluginParameters.h"
#include <array>
#include <cmath>
#define AMBIVALENCE_USE_STAGE2_ABSORPTION 1
namespace FDNReverb {
enum class ReverbTopology { Room, Hall, Plate, Spring, Goldfoil };
// -----------------------------------------------------------------------------
// BandlimitedNoiseLFO: color noise + 1 IIR LPF
// -----------------------------------------------------------------------------
struct BandlimitedNoiseLFO {
uint32_t state{ 12345u };
float smoothed{ 0.0f };
float rateMultiplier{ 1.0f };
inline float nextNoise() noexcept {
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
return static_cast<float>(state) * 2.3283064365386963e-10f * 2.0f - 1.0f;
}
inline float tick(float lpfCoeff) noexcept {
smoothed += (nextNoise() - smoothed) * lpfCoeff;
return smoothed;
}
};
// -----------------------------------------------------------------------------
// ChorusLFO: sine-wave phase (modulation)
// -----------------------------------------------------------------------------
struct ChorusLFO {
float phase{ 0.0f };
float phaseInc{ 0.0f };
float rateScale{ 1.0f }; // per-channel rate coefficient (multiplier)
// * CPU: std::sin() replaced by a parabolic approximation (max error ~0.06%, 5-10x faster)
inline float tick() noexcept {
phase += phaseInc;
if (phase >= 1.0f) phase -= 1.0f;
// Parabolic sine: phase [0,1) -> sin(2pi.phase)
const float x = phase < 0.5f ? phase : phase - 1.0f;
const float para = 16.0f * x * (0.5f - std::abs(x));
return para * (0.775f + 0.225f * std::abs(para));
}
};
class UniversalEngine {
public:
UniversalEngine();
void prepare(double sampleRate, int maxBlockSize);
void reset();
void setParams(const DSPParams& p);
void processBlock(const float* inL, const float* inR,
float* outL, float* outR, int numSamples) noexcept;
std::array<float, NUM_BANDS> getEffectiveRT60() const noexcept { return effectiveRT60; }
float getD50() const noexcept { return acousticMetrics.getD50(); }
float getC50() const noexcept { return acousticMetrics.getC50(); }
float getC80() const noexcept { return acousticMetrics.getC80(); }
float getEDT() const noexcept { return theoreticalEDT; }
const AcousticMetrics& getAcousticMetrics() const noexcept { return acousticMetrics; }
int getERTapCount() const noexcept { return currentERTapCount; }
float getERTapDelaySamples(int index) const noexcept {
return (index >= 0 && index < currentERTapCount) ? currentERDelaySamples[index] : 0.0f;
}
float getERTapGain(int index) const noexcept {
return (index >= 0 && index < currentERTapCount) ? currentERGains[index] : 0.0f;
}
double getSampleRate() const noexcept { return fs; }
bool isERBypassed() const noexcept { return bypassER; }
private:
void updateTopologyAndRouting();
void calculatePrimePowerDelays();
inline void fastWalshHadamardTransform(std::array<float, 16>& v) noexcept;
inline void applySignFlipping(std::array<float, 16>& v) noexcept;
// --- FDN loop saturation ---
inline static float processMicroSaturation(float x) noexcept {
constexpr float kInScale = 0.15f;
constexpr float kOutScale = 1.0f / kInScale;
const float xs = x * kInScale;
if (xs > 3.0f) return kOutScale;
if (xs < -3.0f) return -kOutScale;
const float xsq = xs * xs;
return (xs * (27.0f + xsq) / (27.0f + 9.0f * xsq)) * kOutScale;
}
DelayMemoryPool memoryPool;
double fs{ 48000.0 };
DSPParams activeParams;
ReverbTopology currentTopology{ ReverbTopology::Room };
static constexpr int FDN_ORDER = 16;
static constexpr int SERIAL_APF_STAGES = 3; // * Allpass stages
// * PreDelay (max 500 ms)
LinearDelayLine preDelayLine;
float preDelaySamples{ 0.0f };
LinearDelayLine erDelay;
std::array<float, 16> erTaps;
std::array<LinearDelayLine, 4> inputDiffusers;
std::array<ThiranDelayLine, FDN_ORDER> fdnDelays; // * Thiran allpass interpolation
std::array<std::array<LinearDelayLine, SERIAL_APF_STAGES>, FDN_ORDER> nestedAllpassDelays;
int currentERTapCount{ 0 };
std::array<float, MAX_ER_TAPS> currentERDelaySamples;
std::array<float, MAX_ER_TAPS> currentERGains;
OutputLimiter outputLimiter;
OutputEQ outputEQ; // * Phase 5 added
float duckingEnvelope{ 0.0f };
float duckingAttackCoeff{ 0.0f };
float duckingReleaseCoeff{ 0.0f };
#if AMBIVALENCE_USE_STAGE2_ABSORPTION
std::array<std::array<BiquadState, ABSO_STAGES_S2>, FDN_ORDER> absorptionFiltersS2;
std::array<std::array<BiquadCoeffs, ABSO_STAGES_S2>, FDN_ORDER> currentAbsorptionCoeffsS2;
#else
std::array<BiquadState, FDN_ORDER> absorptionFilters;
std::array<BiquadCoeffs, FDN_ORDER> currentAbsorptionCoeffs;
#endif
std::array<BandlimitedNoiseLFO, FDN_ORDER> lfos;
std::array<ChorusLFO, FDN_ORDER> chorusLFOs; // * modulation
std::array<float, FDN_ORDER> fdnBaseDelaySamples;
std::array<float, FDN_ORDER> fbVec;
float apfGain{ 0.618f };
bool bypassER{ false };
bool bypassInputDiffusers{ false }; // * new: default false
float lateMixScale{ 1.0f };
float lateMakeupGainLinear{ 1.0f };
// * Phase 5 addition: Diffusion
float diffusionSensitivity{ 1.0f };
// * metallic sound: DecayTime depends on parameters
float microSatBlend{ 1.0f }; // FDN loop saturation blend (0 = bypass, 1 = full)
float modDepthScale{ 1.0f }; // modulation depth scale (increases with Decay time)
// * DC: prevent DC accumulation in the FDN loop
std::array<float, FDN_ORDER> dcX1;
std::array<float, FDN_ORDER> dcY1;
float dcBlockerCoeff{ 0.999f };
// * soft-knee compression: in the FDN feedback loop
std::array<float, FDN_ORDER> fdnRmsEnv;
float rmsCoeff{ 0.002f };
std::array<float, NUM_BANDS> effectiveRT60;
float theoreticalEDT{ 0.0f };
AcousticMetrics acousticMetrics;
Saturator saturatorL;
Saturator saturatorR;
};
} // namespace FDNReverb

View file

@ -0,0 +1,354 @@
#include "AmbivalenceUI.h"
#include "../PluginProcessor.h"
// --- AmbivalenceLookAndFeel ---------------------------------------------
AmbivalenceLookAndFeel::AmbivalenceLookAndFeel()
{
setColour(juce::Slider::backgroundColourId, AmbivalenceColors::ArcTrack);
setColour(juce::Slider::thumbColourId, AmbivalenceColors::Accent);
setColour(juce::Slider::trackColourId, AmbivalenceColors::ArcFill);
setColour(juce::Label::textColourId, AmbivalenceColors::TextSecondary);
setColour(juce::ComboBox::backgroundColourId, AmbivalenceColors::Surface);
setColour(juce::ComboBox::textColourId, AmbivalenceColors::TextPrimary);
setColour(juce::ComboBox::outlineColourId, AmbivalenceColors::Border);
mainFont = juce::Font(juce::FontOptions("Helvetica Neue", 11.f, juce::Font::plain));
}
void AmbivalenceLookAndFeel::drawRotarySlider(juce::Graphics& g,
int x, int y, int w, int h,
float sliderPos, float startAngle, float endAngle, juce::Slider&)
{
auto b = juce::Rectangle<float>((float)x, (float)y, (float)w, (float)h).reduced(4.f);
float cx = b.getCentreX(), cy = b.getCentreY();
float r = juce::jmin(b.getWidth(), b.getHeight()) * 0.45f;
float th = r * 0.22f;
juce::Path track;
track.addCentredArc(cx, cy, r, r, 0.f, startAngle, endAngle, true);
g.setColour(AmbivalenceColors::ArcTrack);
g.strokePath(track, juce::PathStrokeType(th,
juce::PathStrokeType::curved, juce::PathStrokeType::rounded));
float angle = startAngle + sliderPos * (endAngle - startAngle);
juce::Path fill;
fill.addCentredArc(cx, cy, r, r, 0.f, startAngle, angle, true);
juce::ColourGradient grad(AmbivalenceColors::AccentBlue, cx - r, cy,
AmbivalenceColors::Accent, cx + r, cy, false);
g.setGradientFill(grad);
g.strokePath(fill, juce::PathStrokeType(th,
juce::PathStrokeType::curved, juce::PathStrokeType::rounded));
g.setColour(AmbivalenceColors::Panel);
g.fillEllipse(cx - r * 0.28f, cy - r * 0.28f, r * 0.56f, r * 0.56f);
float ix = cx + r * 0.6f * std::sin(angle);
float iy = cy - r * 0.6f * std::cos(angle);
g.setColour(AmbivalenceColors::TextPrimary);
g.drawLine(cx, cy, ix, iy, 2.f);
}
void AmbivalenceLookAndFeel::drawLinearSlider(juce::Graphics& g,
int x, int y, int w, int h,
float sliderPos, float, float, juce::Slider::SliderStyle, juce::Slider&)
{
auto b = juce::Rectangle<int>(x, y, w, h).toFloat();
float ty = b.getCentreY() - 2.f;
g.setColour(AmbivalenceColors::ArcTrack);
g.fillRoundedRectangle(b.getX(), ty, b.getWidth(), 4.f, 2.f);
g.setColour(AmbivalenceColors::Accent);
g.fillRoundedRectangle(b.getX(), ty, sliderPos - b.getX(), 4.f, 2.f);
float r = 7.f;
g.setColour(AmbivalenceColors::TextPrimary);
g.fillEllipse(sliderPos - r, b.getCentreY() - r, r * 2.f, r * 2.f);
}
void AmbivalenceLookAndFeel::drawComboBox(juce::Graphics& g,
int w, int h, bool isDown, int, int, int, int, juce::ComboBox&)
{
auto b = juce::Rectangle<int>(0, 0, w, h).toFloat();
g.setColour(isDown ? AmbivalenceColors::Panel : AmbivalenceColors::Surface);
g.fillRoundedRectangle(b, 3.f);
g.setColour(AmbivalenceColors::Border);
g.drawRoundedRectangle(b.reduced(0.5f), 3.f, 1.f);
juce::Path arrow;
arrow.addTriangle(w - 16.f, h * 0.5f - 3.f,
w - 8.f, h * 0.5f - 3.f,
w - 12.f, h * 0.5f + 3.f);
g.setColour(AmbivalenceColors::TextSecondary);
g.fillPath(arrow);
}
void AmbivalenceLookAndFeel::positionComboBoxText(juce::ComboBox& box, juce::Label& label) {
label.setBounds(6, 1, box.getWidth() - 22, box.getHeight() - 2);
label.setFont(getComboBoxFont(box));
}
juce::Font AmbivalenceLookAndFeel::getLabelFont(juce::Label&) { return mainFont.withHeight(10.f); }
juce::Font AmbivalenceLookAndFeel::getComboBoxFont(juce::ComboBox&) { return mainFont.withHeight(11.f); }
void AmbivalenceLookAndFeel::drawGroupComponentOutline(juce::Graphics& g,
int w, int h, const juce::String& text,
const juce::Justification&, juce::GroupComponent&)
{
float textH = 12.f, indent = 8.f, yOff = textH * 0.5f;
juce::Path p;
p.startNewSubPath(indent + 4.f, yOff); p.lineTo(indent, yOff);
p.lineTo(indent, (float)h - 1.f);
p.lineTo((float)w - indent, (float)h - 1.f);
p.lineTo((float)w - indent, yOff);
// * redraw after changes
juce::GlyphArrangement ga;
ga.addLineOfText(mainFont.withHeight(textH), text, 0.f, 0.f);
float tw = ga.getBoundingBox(0, -1, true).getWidth() + 6.f;
p.lineTo(indent + 14.f + tw, yOff);
g.setColour(AmbivalenceColors::Border);
g.strokePath(p, juce::PathStrokeType(1.f));
g.setColour(AmbivalenceColors::TextSecondary);
g.setFont(mainFont.withHeight(textH).boldened());
g.drawText(text, (int)(indent + 14.f), 0, (int)tw, (int)textH,
juce::Justification::centredLeft);
}
// --- RT60Visualizer --------------------------------------------------
RT60Visualizer::RT60Visualizer() {
displayRT60.fill(1.0f);
startTimerHz(30);
}
RT60Visualizer::~RT60Visualizer() { stopTimer(); }
void RT60Visualizer::timerCallback() {
if (!processor) return;
auto live = processor->getRT60ForDisplay();
for (int i = 0; i < FDNReverb::NUM_BANDS; ++i)
displayRT60[i] += 0.25f * (live[i] - displayRT60[i]);
// * dynamic Y axis above : current maximum RT60 value x 1.3 smoothly
float maxVal = *std::max_element(displayRT60.begin(), displayRT60.end());
float targetMax = std::max(MAX_RT60_DISPLAY_FLOOR, maxVal * 1.3f);
// exponential ( rise quickly , fall gentle -> frequently )
float smoothFactor = (targetMax > dynamicMaxRT60) ? 0.15f : 0.03f;
dynamicMaxRT60 += smoothFactor * (targetMax - dynamicMaxRT60);
repaint();
}
void RT60Visualizer::paint(juce::Graphics& g)
{
auto b = getLocalBounds().toFloat().reduced(2.f);
float W = b.getWidth(), H = b.getHeight();
float x0 = b.getX(), y0 = b.getY();
g.setColour(AmbivalenceColors::Surface);
g.fillRoundedRectangle(b, 4.f);
g.setColour(AmbivalenceColors::Border);
g.drawRoundedRectangle(b.reduced(0.5f), 4.f, 1.f);
// * dynamic Y axis
float logMin = std::log10(MIN_RT60_DISPLAY);
float logMax = std::log10(dynamicMaxRT60);
// grid value dynamic Y axis
// fixed value dynamicMaxRT60 below drawing
static constexpr float kAllGridVals[] = {
0.1f, 0.3f, 0.5f, 1.0f, 2.0f, 4.0f,
8.0f, 12.0f, 16.0f, 20.0f
};
// grid
g.setColour(AmbivalenceColors::Separator);
for (float v : kAllGridVals) {
if (v > dynamicMaxRT60 * 1.05f) break;
float ny = 1.f - (std::log10(v) - logMin) / (logMax - logMin);
g.drawHorizontalLine((int)(y0 + ny * H), x0 + 36.f, x0 + W - 4.f);
}
// frequency label (X axis )
g.setFont(8.5f);
g.setColour(AmbivalenceColors::TextSecondary);
static const char* fLbls[] = {
"31","63","125","250","500","1k","2k","4k","8k","16k"
};
for (int i = 0; i < FDNReverb::NUM_BANDS; ++i) {
float px = x0 + 36.f + (float)i / (FDNReverb::NUM_BANDS - 1) * (W - 40.f);
g.drawText(fLbls[i], (int)(px - 12.f), (int)(y0 + H - 14.f),
24, 13, juce::Justification::centred);
}
// seconds label (Y axis ) - dynamic
for (float v : kAllGridVals) {
if (v > dynamicMaxRT60 * 1.05f) break;
float ny = 1.f - (std::log10(v) - logMin) / (logMax - logMin);
float py = y0 + ny * H;
juce::String lbl = (v < 1.f)
? juce::String(v, 1) + "s"
: (v < 10.f ? juce::String(v, 1) : juce::String((int)v)) + "s";
g.drawText(lbl, (int)(x0 + 2.f), (int)(py - 7.f), 32, 14,
juce::Justification::centredLeft);
}
auto plotCurve = [&](const std::array<float, FDNReverb::NUM_BANDS>& rt60,
juce::Colour col, float thick)
{
juce::Path path;
bool first = true;
for (int i = 0; i < FDNReverb::NUM_BANDS; ++i) {
float v = std::clamp(rt60[i], MIN_RT60_DISPLAY, dynamicMaxRT60);
float ny = 1.f - (std::log10(v) - logMin) / (logMax - logMin);
float px = x0 + 36.f + (float)i / (FDNReverb::NUM_BANDS - 1) * (W - 40.f);
float py = y0 + ny * H;
if (first) { path.startNewSubPath(px, py); first = false; }
else path.lineTo(px, py);
}
g.setColour(col);
g.strokePath(path, juce::PathStrokeType(thick,
juce::PathStrokeType::curved, juce::PathStrokeType::rounded));
for (int i = 0; i < FDNReverb::NUM_BANDS; ++i) {
float v = std::clamp(rt60[i], MIN_RT60_DISPLAY, dynamicMaxRT60);
float ny = 1.f - (std::log10(v) - logMin) / (logMax - logMin);
float px = x0 + 36.f + (float)i / (FDNReverb::NUM_BANDS - 1) * (W - 40.f);
float py = y0 + ny * H;
g.fillEllipse(px - 3.f, py - 3.f, 6.f, 6.f);
}
};
// source preset curve (from the selected algorithm)
if (processor) {
int algo = (int)*processor->apvts.getRawParameterValue("algorithm");
auto& preset = *FDNReverb::ALL_PRESETS[
juce::jlimit(0, FDNReverb::NUM_ALGORITHMS - 1, algo)];
plotCurve(preset.acoustics.rt60,
AmbivalenceColors::TextSecondary.withAlpha(0.5f), 1.f);
}
// current RT60 curve (measured)
plotCurve(displayRT60, AmbivalenceColors::Accent, 2.f);
// top right title
g.setColour(AmbivalenceColors::TextSecondary);
g.setFont(9.f);
g.drawText("RT60 (s) per band",
(int)x0 + 36, (int)y0 + 3, (int)W - 40, 12,
juce::Justification::right);
}
// --- VUMeter ---------------------------------------------------------
VUMeter::VUMeter(const juce::String& lbl, Side s) : label(lbl), side(s) {}
void VUMeter::paint(juce::Graphics& g)
{
auto b = getLocalBounds().toFloat().reduced(1.f);
g.setColour(AmbivalenceColors::Surface);
g.fillRoundedRectangle(b, 3.f);
float bx = b.getX() + 22.f, bw = b.getWidth() - 22.f;
auto bar = [&](float y, float level) {
float n = juce::jlimit(0.f, 1.f, juce::jmap(
juce::Decibels::gainToDecibels(level + 1e-9f), -60.f, 0.f, 0.f, 1.f));
g.setColour(AmbivalenceColors::ArcTrack);
g.fillRoundedRectangle(bx, y, bw, 7.f, 2.f);
juce::ColourGradient gr(AmbivalenceColors::AccentBlue, bx, y,
AmbivalenceColors::Accent, bx + bw, y, false);
g.setGradientFill(gr);
g.fillRoundedRectangle(bx, y, bw * n, 7.f, 2.f);
};
bar(b.getY() + 2.f, levelL);
bar(b.getY() + 11.f, levelR);
g.setColour(AmbivalenceColors::TextSecondary);
g.setFont(8.f);
g.drawText(label, (int)b.getX(), (int)b.getY(), 20, (int)b.getHeight(),
juce::Justification::centredLeft);
}
// --- ArcKnob ---------------------------------------------------------
void ArcKnob::build(juce::AudioProcessorValueTreeState& apvts,
const juce::String& paramID,
const juce::String& labelText,
juce::Component* parent,
AmbivalenceLookAndFeel& laf)
{
slider.setSliderStyle(juce::Slider::RotaryHorizontalVerticalDrag);
slider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 62, 14);
slider.setLookAndFeel(&laf);
slider.setColour(juce::Slider::textBoxTextColourId,
AmbivalenceColors::TextSecondary);
slider.setColour(juce::Slider::textBoxOutlineColourId,
juce::Colours::transparentBlack);
parent->addAndMakeVisible(slider);
label.setText(labelText, juce::dontSendNotification);
label.setJustificationType(juce::Justification::centred);
label.setFont(juce::Font(juce::FontOptions(9.f)));
label.setColour(juce::Label::textColourId, AmbivalenceColors::TextSecondary);
parent->addAndMakeVisible(label);
// * after changes
attachment.reset(
new juce::AudioProcessorValueTreeState::SliderAttachment(
apvts, paramID, slider));
}
// --- AlgorithmSelector -----------------------------------------------
AlgorithmSelector::AlgorithmSelector(juce::AudioProcessorValueTreeState& a)
: apvts(a)
{
static const char* names[] = {
"ROOM1","ROOM2","HALL1","HALL2","PLATE","SPRING","GOLDFOIL"
};
for (int i = 0; i < FDNReverb::NUM_ALGORITHMS; ++i) {
buttons[i].setButtonText(names[i]);
addAndMakeVisible(buttons[i]);
int idx = i;
buttons[i].onClick = [this, idx] {
if (auto* param = apvts.getParameter("algorithm"))
param->setValueNotifyingHost(
(float)idx / (float)(FDNReverb::NUM_ALGORITHMS - 1));
};
}
apvts.addParameterListener("algorithm", this);
currentAlgo = juce::roundToInt(
*apvts.getRawParameterValue("algorithm")
* (FDNReverb::NUM_ALGORITHMS - 1));
}
AlgorithmSelector::~AlgorithmSelector() {
apvts.removeParameterListener("algorithm", this);
}
void AlgorithmSelector::parameterChanged(const juce::String&, float newVal) {
int newAlgo = juce::jlimit(0, FDNReverb::NUM_ALGORITHMS - 1,
juce::roundToInt(newVal));
juce::MessageManager::callAsync([this, newAlgo] {
currentAlgo = newAlgo;
updateButtonColors();
});
}
void AlgorithmSelector::updateButtonColors() {
for (int i = 0; i < FDNReverb::NUM_ALGORITHMS; ++i) {
bool on = (i == currentAlgo);
buttons[i].setColour(juce::TextButton::buttonColourId,
on ? AmbivalenceColors::Accent : AmbivalenceColors::Surface);
buttons[i].setColour(juce::TextButton::textColourOffId,
on ? AmbivalenceColors::Background : AmbivalenceColors::TextSecondary);
buttons[i].repaint();
}
}
void AlgorithmSelector::paint(juce::Graphics& g) {
g.setColour(AmbivalenceColors::Surface);
g.fillRoundedRectangle(getLocalBounds().toFloat(), 4.f);
}
void AlgorithmSelector::resized() {
auto area = getLocalBounds().reduced(2);
int btnW = area.getWidth() / FDNReverb::NUM_ALGORITHMS;
for (int i = 0; i < FDNReverb::NUM_ALGORITHMS; ++i)
buttons[i].setBounds(area.getX() + i * btnW, area.getY(),
btnW - 1, area.getHeight());
updateButtonColors();
}

110
Source/GUI/AmbivalenceUI.h Normal file
View file

@ -0,0 +1,110 @@
#pragma once
#include <JuceHeader.h>
#include "../AlgorithmPresets.h"
class FDNReverbAudioProcessor;
// --- Ambivalence Design System ------------------------------------------
namespace AmbivalenceColors {
const juce::Colour Background{ 0xFF1A1A1A };
const juce::Colour Surface{ 0xFF242424 };
const juce::Colour Panel{ 0xFF2C2C2C };
const juce::Colour Border{ 0xFF3C3C3C };
const juce::Colour Accent{ 0xFFFF6B00 };
const juce::Colour AccentBlue{ 0xFF4090FF };
const juce::Colour TextPrimary{ 0xFFE8E8E8 };
const juce::Colour TextSecondary{ 0xFF888888 };
const juce::Colour ArcTrack{ 0xFF3A3A3A };
const juce::Colour ArcFill{ 0xFFFF6B00 };
const juce::Colour Separator{ 0xFF383838 };
}
// --- Ambivalence LookAndFeel --------------------------------------------
class AmbivalenceLookAndFeel : public juce::LookAndFeel_V4
{
public:
AmbivalenceLookAndFeel();
void drawRotarySlider(juce::Graphics&, int x, int y, int w, int h,
float sliderPos, float startAngle, float endAngle,
juce::Slider&) override;
void drawLinearSlider(juce::Graphics&, int x, int y, int w, int h,
float sliderPos, float, float,
juce::Slider::SliderStyle, juce::Slider&) override;
void drawComboBox(juce::Graphics&, int w, int h, bool isDown,
int, int, int, int, juce::ComboBox&) override;
void positionComboBoxText(juce::ComboBox&, juce::Label&) override;
juce::Font getLabelFont(juce::Label&) override;
juce::Font getComboBoxFont(juce::ComboBox&) override;
void drawGroupComponentOutline(juce::Graphics&, int w, int h,
const juce::String&, const juce::Justification&,
juce::GroupComponent&) override;
private:
juce::Font mainFont;
};
// --- RT60 Visualizer -------------------------------------------------
class RT60Visualizer : public juce::Component, private juce::Timer
{
public:
RT60Visualizer();
~RT60Visualizer() override;
void setProcessor(FDNReverbAudioProcessor* p) { processor = p; }
void paint(juce::Graphics&) override;
private:
void timerCallback() override;
FDNReverbAudioProcessor* processor{ nullptr };
std::array<float, FDNReverb::NUM_BANDS> displayRT60;
static constexpr float MIN_RT60_DISPLAY = 0.05f;
static constexpr float MAX_RT60_DISPLAY_FLOOR = 4.0f; // Y axis above value
// * dynamic Y axis above : effectiveRT60 maximum value smoothly
float dynamicMaxRT60{ MAX_RT60_DISPLAY_FLOOR };
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(RT60Visualizer)
};
// --- VU Meter --------------------------------------------------------
class VUMeter : public juce::Component
{
public:
enum class Side { Input, Output };
VUMeter(const juce::String& label, Side side);
void paint(juce::Graphics&) override;
void setLevels(float l, float r) noexcept { levelL = l; levelR = r; }
private:
juce::String label;
Side side;
float levelL{ 0.f }, levelR{ 0.f };
};
// --- Labelled Arc Knob -----------------------------------------------
struct ArcKnob {
juce::Slider slider;
juce::Label label;
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> attachment;
void build(juce::AudioProcessorValueTreeState& apvts,
const juce::String& paramID,
const juce::String& labelText,
juce::Component* parent,
AmbivalenceLookAndFeel& laf);
};
// --- Algorithm Selector ----------------------------------------------
class AlgorithmSelector : public juce::Component,
private juce::AudioProcessorValueTreeState::Listener
{
public:
AlgorithmSelector(juce::AudioProcessorValueTreeState& apvts);
~AlgorithmSelector() override;
void paint(juce::Graphics&) override;
void resized() override;
private:
void parameterChanged(const juce::String&, float) override;
void updateButtonColors();
std::array<juce::TextButton, FDNReverb::NUM_ALGORITHMS> buttons;
juce::AudioProcessorValueTreeState& apvts;
int currentAlgo{ 0 };
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AlgorithmSelector)
};

View file

@ -0,0 +1,324 @@
#include "DecayCurveViz.h"
DecayCurveViz::DecayCurveViz() {
cachedERDelayMs.fill(0.0f);
cachedERGains.fill(0.0f);
startTimerHz(15);
}
DecayCurveViz::~DecayCurveViz() {
stopTimer();
}
void DecayCurveViz::timerCallback() {
if (processor == nullptr) return;
const auto& engine = processor->getEngine();
auto rt60 = engine.getEffectiveRT60();
cachedRT60Mid = std::max(0.1f, rt60[4]);
cachedERBypassed = engine.isERBypassed();
cachedERTapCount = engine.getERTapCount();
if (cachedERTapCount > MAX_DISPLAY_TAPS)
cachedERTapCount = MAX_DISPLAY_TAPS;
double sr = engine.getSampleRate();
if (sr < 1.0) sr = 48000.0;
for (int i = 0; i < cachedERTapCount; ++i) {
float delaySamples = engine.getERTapDelaySamples(i);
cachedERDelayMs[i] = delaySamples / static_cast<float>(sr) * 1000.0f;
cachedERGains[i] = engine.getERTapGain(i);
}
repaint();
}
void DecayCurveViz::resized() {}
void DecayCurveViz::paint(juce::Graphics& g)
{
auto bounds = getLocalBounds().toFloat();
if (bounds.getWidth() < 10.0f || bounds.getHeight() < 10.0f) return;
g.fillAll(AmbivalenceColors::Background);
const float topMargin = 10.0f;
const float bottomMargin = 18.0f;
const float leftMargin = 30.0f;
const float rightMargin = 8.0f;
const float plotX = bounds.getX() + leftMargin;
const float plotY = bounds.getY() + topMargin;
const float plotW = bounds.getWidth() - leftMargin - rightMargin;
const float plotH = bounds.getHeight() - topMargin - bottomMargin;
const float maxTimeSec = juce::jlimit(0.5f, 8.0f, cachedRT60Mid * 1.5f);
const float minDB = -60.0f;
const float maxDB = 0.0f;
// -------------------------------------------------------------------------
// time axis
// 0~splitSec -> full width splitRatio expanded (ER zone )
// splitSec~max -> width (Late zone )
// -------------------------------------------------------------------------
constexpr float splitSec = 0.20f; // 200ms expanded
constexpr float splitRatio = 0.30f; // full width 30% ER zone
auto timeToX = [&](float timeSec) -> float {
if (timeSec <= splitSec) {
const float ratio = timeSec / splitSec;
return plotX + ratio * plotW * splitRatio;
}
else {
const float lateRange = maxTimeSec - splitSec;
if (lateRange <= 0.0f) return plotX + plotW;
const float ratio = (timeSec - splitSec) / lateRange;
return plotX + plotW * splitRatio + ratio * plotW * (1.0f - splitRatio);
}
};
auto dbToY = [&](float db) -> float {
const float normalized = (db - minDB) / (maxDB - minDB);
return plotY + (1.0f - normalized) * plotH;
};
// --- ER zone background ---
{
const float erZoneW = plotW * splitRatio;
g.setColour(juce::Colour(0xFF1A2535));
g.fillRect(plotX, plotY, erZoneW, plotH);
}
// --- grid : horizontal (dB) ---
g.setColour(AmbivalenceColors::Separator.withAlpha(0.3f));
for (float db = 0.0f; db >= -60.0f; db -= 20.0f)
g.drawHorizontalLine((int)dbToY(db), plotX, plotX + plotW);
// --- grid : ER zone vertical (ms) ---
{
static const float erGridMs[] = { 20.0f, 50.0f, 100.0f, 150.0f, 200.0f };
g.setColour(AmbivalenceColors::Separator.withAlpha(0.5f));
for (float ms : erGridMs) {
const float t = ms * 0.001f;
if (t >= maxTimeSec) break;
g.drawVerticalLine((int)timeToX(t), plotY, plotY + plotH);
}
}
// --- grid : Late zone vertical (s) ---
float timeStep;
if (maxTimeSec <= 2.0f) timeStep = 0.5f;
else if (maxTimeSec <= 4.0f) timeStep = 1.0f;
else timeStep = 2.0f;
g.setColour(AmbivalenceColors::Separator.withAlpha(0.3f));
for (float t = splitSec + timeStep; t <= maxTimeSec; t += timeStep)
g.drawVerticalLine((int)timeToX(t), plotY, plotY + plotH);
// --- ---
{
const float splitX = timeToX(splitSec);
g.setColour(AmbivalenceColors::Separator.withAlpha(0.9f));
g.drawVerticalLine((int)splitX, plotY, plotY + plotH);
}
// --- axis label ---
g.setFont(juce::Font(juce::FontOptions(8.5f)));
g.setColour(AmbivalenceColors::TextSecondary.withAlpha(0.6f));
for (float db = 0.0f; db >= -60.0f; db -= 20.0f) {
const float y = dbToY(db);
g.drawText(juce::String((int)db) + "dB",
(int)(plotX - leftMargin + 2), (int)(y - 6),
(int)(leftMargin - 4), 12,
juce::Justification::centredRight);
}
// ER zone time label (ms)
{
static const float erGridMs[] = { 20.0f, 50.0f, 100.0f, 150.0f, 200.0f };
for (float ms : erGridMs) {
const float t = ms * 0.001f;
if (t >= maxTimeSec) break;
const float x = timeToX(t);
g.drawText(juce::String((int)ms) + "ms",
(int)(x - 20), (int)(plotY + plotH + 2),
40, 14, juce::Justification::centred);
}
}
// Late zone time label (s)
for (float t = splitSec + timeStep; t <= maxTimeSec; t += timeStep) {
const float x = timeToX(t);
g.drawText(juce::String(t, 1) + "s",
(int)(x - 20), (int)(plotY + plotH + 2),
40, 14, juce::Justification::centred);
}
// -------------------------------------------------------------------------
// Late Reverb decay curve ( 2D gradient )
// -------------------------------------------------------------------------
{
const int numPoints = 80;
juce::Colour orangeColor = AmbivalenceColors::Accent;
juce::Path latePath;
latePath.startNewSubPath(plotX, dbToY(maxDB));
for (int i = 0; i <= numPoints; ++i) {
const float t = (i / static_cast<float>(numPoints)) * maxTimeSec;
const float db = std::max(minDB, -60.0f * t / cachedRT60Mid);
latePath.lineTo(timeToX(t), dbToY(db));
}
latePath.lineTo(timeToX(maxTimeSec), dbToY(minDB));
latePath.lineTo(plotX, dbToY(minDB));
latePath.closeSubPath();
juce::ColourGradient lateGrad(
orangeColor.withAlpha(0.55f), plotX, plotY,
orangeColor.withAlpha(0.0f), plotX + plotW, plotY + plotH,
false);
g.setGradientFill(lateGrad);
g.fillPath(latePath);
juce::Path lateOutline;
lateOutline.startNewSubPath(plotX, dbToY(maxDB));
for (int i = 0; i <= numPoints; ++i) {
const float t = (i / static_cast<float>(numPoints)) * maxTimeSec;
const float db = std::max(minDB, -60.0f * t / cachedRT60Mid);
lateOutline.lineTo(timeToX(t), dbToY(db));
}
g.setColour(orangeColor.withAlpha(0.75f));
g.strokePath(lateOutline, juce::PathStrokeType(1.5f,
juce::PathStrokeType::curved, juce::PathStrokeType::rounded));
}
// -------------------------------------------------------------------------
// ER drawing (* )
// : vertical line 2px + marker
// : wide 5px + marker + envelope fill
// -------------------------------------------------------------------------
if (!cachedERBypassed && cachedERTapCount > 0) {
const juce::Colour blueColor = juce::Colour::fromRGB(80, 160, 230);
// -- ER envelope fill region --
if (cachedERTapCount >= 2) {
juce::Path erFill;
bool started = false;
float lastX = plotX;
for (int t = 0; t < cachedERTapCount; ++t) {
const float timeSec = cachedERDelayMs[t] * 0.001f;
if (timeSec > maxTimeSec) continue;
float gainDB = (cachedERGains[t] > 1e-6f)
? juce::Decibels::gainToDecibels(cachedERGains[t]) : minDB;
gainDB = juce::jlimit(minDB, maxDB, gainDB);
const float x = timeToX(timeSec);
const float y = dbToY(gainDB);
if (!started) {
erFill.startNewSubPath(plotX, dbToY(minDB));
erFill.lineTo(x, y);
started = true;
}
else {
erFill.lineTo(x, y);
}
lastX = x;
}
if (started) {
erFill.lineTo(lastX, dbToY(minDB));
erFill.closeSubPath();
juce::ColourGradient erAreaGrad(
blueColor.withAlpha(0.20f), plotX, plotY,
blueColor.withAlpha(0.03f), plotX + plotW * splitRatio, plotY + plotH,
false);
g.setGradientFill(erAreaGrad);
g.fillPath(erFill);
}
}
// -- : wide + marker --
for (int t = 0; t < cachedERTapCount; ++t) {
const float timeSec = cachedERDelayMs[t] * 0.001f;
if (timeSec > maxTimeSec) continue;
float gainDB = (cachedERGains[t] > 1e-6f)
? juce::Decibels::gainToDecibels(cachedERGains[t]) : minDB;
gainDB = juce::jlimit(minDB, maxDB, gainDB);
const float x = timeToX(timeSec);
const float yTop = dbToY(gainDB);
const float yBottom = dbToY(minDB);
const float barW = 5.0f;
juce::ColourGradient tapGrad(
blueColor.withAlpha(0.90f), x, yTop,
blueColor.withAlpha(0.10f), x, yBottom,
false);
g.setGradientFill(tapGrad);
g.fillRect(x - barW * 0.5f, yTop, barW, yBottom - yTop);
// diamond marker
g.setColour(blueColor);
juce::Path diamond;
diamond.startNewSubPath(x, yTop - 5.0f);
diamond.lineTo(x + 4.0f, yTop);
diamond.lineTo(x, yTop + 3.0f);
diamond.lineTo(x - 4.0f, yTop);
diamond.closeSubPath();
g.fillPath(diamond);
}
// -- ER envelope outline --
if (cachedERTapCount >= 2) {
juce::Path erOutline;
bool started = false;
for (int t = 0; t < cachedERTapCount; ++t) {
const float timeSec = cachedERDelayMs[t] * 0.001f;
if (timeSec > maxTimeSec) continue;
float gainDB = (cachedERGains[t] > 1e-6f)
? juce::Decibels::gainToDecibels(cachedERGains[t]) : minDB;
gainDB = juce::jlimit(minDB, maxDB, gainDB);
const float x = timeToX(timeSec);
const float y = dbToY(gainDB);
if (!started) { erOutline.startNewSubPath(x, y); started = true; }
else erOutline.lineTo(x, y);
}
g.setColour(blueColor.withAlpha(0.65f));
g.strokePath(erOutline, juce::PathStrokeType(1.5f,
juce::PathStrokeType::curved, juce::PathStrokeType::rounded));
}
}
// --- zone label ---
g.setFont(juce::Font(juce::FontOptions(
"Helvetica Neue", 8.0f, juce::Font::bold)));
g.setColour(juce::Colour::fromRGB(80, 160, 230).withAlpha(0.9f));
g.drawText("ER",
(int)(plotX + 4), (int)(plotY + 2),
30, 12, juce::Justification::centredLeft);
{
const float splitX = timeToX(splitSec);
g.setColour(AmbivalenceColors::Accent.withAlpha(0.9f));
g.drawText("LATE",
(int)(splitX + 6), (int)(plotY + 2),
40, 12, juce::Justification::centredLeft);
}
g.setFont(juce::Font(juce::FontOptions(7.5f)));
g.setColour(AmbivalenceColors::TextSecondary.withAlpha(0.4f));
g.drawText("0-200ms (x2)",
(int)(plotX + 2), (int)(plotY + plotH - 14),
(int)(plotW * splitRatio - 4), 12,
juce::Justification::centredLeft);
}

View file

@ -0,0 +1,42 @@
#pragma once
#include <JuceHeader.h>
#include "../PluginProcessor.h"
#include "AmbivalenceUI.h"
class DecayCurveViz : public juce::Component, private juce::Timer {
public:
DecayCurveViz();
~DecayCurveViz() override;
void setProcessor(FDNReverbAudioProcessor* p) noexcept { processor = p; }
void paint(juce::Graphics& g) override;
void resized() override;
private:
void timerCallback() override;
// --- time axis -----------------------------------------------
// time axis :
// 0~splitSec : plotW x splitRatio width expanded
// splitSec~max: width
// ER(0~200ms) 2
float timeToX(float timeSec, float plotX, float plotW,
float maxTimeSec) const noexcept;
FDNReverbAudioProcessor* processor{ nullptr };
float cachedRT60Mid{ 1.0f };
int cachedERTapCount{ 0 };
bool cachedERBypassed{ false };
static constexpr int MAX_DISPLAY_TAPS = 12;
std::array<float, MAX_DISPLAY_TAPS> cachedERDelayMs;
std::array<float, MAX_DISPLAY_TAPS> cachedERGains;
// --- time axis setting ---
// splitSec below time splitRatio width expanded
static constexpr float splitSec = 0.20f; // 0~200ms expanded
static constexpr float splitRatio = 0.30f; // full width 30% ER zone
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(DecayCurveViz)
};

690
Source/PluginEditor.cpp Normal file
View file

@ -0,0 +1,690 @@
#include "PluginProcessor.h"
#include "PluginEditor.h"
#include "BuildInfo.h"
static constexpr int Y_HEADER = 8;
static constexpr int Y_ALGO = 48;
static constexpr int Y_SLABEL1 = 86;
static constexpr int Y_ROW1 = 104;
static constexpr int Y_SLABEL2 = 204;
static constexpr int Y_ROW2 = 222;
static constexpr int Y_SEP = 322;
static constexpr int Y_VIZ = 326;
static constexpr int SEC_TIME = 8;
static constexpr int SEC_FREQUENCY = 254;
static constexpr int SEC_DIFFUSION = 418;
static constexpr int SEC_STEREO = 664;
static constexpr int SEC_CHARACTER = 746;
static constexpr int SEP_TF = 245;
static constexpr int SEP_FD = 409;
static constexpr int SEP_DS = 655;
static constexpr int SEP_SC = 737;
// -----------------------------------------------------------------------------
// constructor
// -----------------------------------------------------------------------------
FDNReverbEditor::FDNReverbEditor(FDNReverbAudioProcessor& p)
: AudioProcessorEditor(&p),
audioProcessor(p),
algoSelector(p.apvts),
vuIn("IN", VUMeter::Side::Input),
vuOut("OUT", VUMeter::Side::Output)
{
setLookAndFeel(&laf);
setSize(W, H);
// -- Title --
titleLabel.setText("AMBIVALENCE 1.1", juce::dontSendNotification);
titleLabel.setFont(juce::Font(juce::FontOptions(
"Helvetica Neue", 14.f, juce::Font::bold)));
titleLabel.setColour(juce::Label::textColourId, AmbivalenceColors::TextPrimary);
addAndMakeVisible(titleLabel);
addAndMakeVisible(algoSelector);
auto BK = [&](ArcKnob& k, const char* id, const char* lbl) {
k.build(p.apvts, id, lbl, this, laf);
};
BK(kPreDelay, "predelay", "PRE-DELAY");
BK(kRoomSize, "roomsize", "ROOM SIZE");
BK(kDecay, "decaytime", "DECAY");
BK(kHFDamp, "hfdamping", "HF DAMP");
BK(kLFAbsorb, "lfabsorption", "LF ABSORB");
BK(kDiffusion, "diffusion", "DIFFUSION");
BK(kModAmt, "modamount", "MOD AMT");
BK(kModRate, "modrate", "MOD RATE");
BK(kStereoW, "stereowidth", "WIDTH");
BK(kERLevel, "erlevel", "ER LEVEL");
BK(kSaturation, "saturation", "SATURATE");
BK(kWet, "wetlevel", "WET");
BK(kDry, "drylevel", "DRY");
BK(kDuckAmt, "duckamount", "AMOUNT");
BK(kDuckThr, "duckthresh", "THRESH");
BK(kDuckAtt, "duckattack", "ATTACK");
BK(kDuckRel, "duckrelease", "RELEASE");
BK(kLoCutNorm, "locut", "LO CUT");
BK(kHiCutNorm, "hicut", "HI CUT");
// -- ProMode button --
proModeButton.setButtonText("PRO");
proModeButton.setClickingTogglesState(true);
proModeButton.setColour(juce::TextButton::buttonOnColourId, AmbivalenceColors::Accent);
proModeButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface);
proModeButton.setColour(juce::TextButton::textColourOnId, AmbivalenceColors::Background);
proModeButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::TextSecondary);
addAndMakeVisible(proModeButton);
proModeAttachment.reset(
new juce::AudioProcessorValueTreeState::ButtonAttachment(
p.apvts, "promode", proModeButton));
// -- ER SOLO button --
erSoloButton.setButtonText("ER SOLO");
erSoloButton.setClickingTogglesState(true);
erSoloButton.setColour(juce::TextButton::buttonOnColourId, AmbivalenceColors::AccentBlue);
erSoloButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface);
erSoloButton.setColour(juce::TextButton::textColourOnId, AmbivalenceColors::Background);
erSoloButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::TextSecondary);
addAndMakeVisible(erSoloButton);
erSoloAttachment.reset(
new juce::AudioProcessorValueTreeState::ButtonAttachment(
p.apvts, "ersolo", erSoloButton));
// -- ProMode: RT60 band --
static const char* rtBandIDs[] = {
"rtband0","rtband1","rtband2","rtband3","rtband4",
"rtband5","rtband6","rtband7","rtband8","rtband9"
};
static const char* rtBandLbls[] = {
"31Hz","63Hz","125Hz","250Hz","500Hz",
"1kHz","2kHz","4kHz","8kHz","16kHz"
};
for (int i = 0; i < 10; ++i)
kRTBands[i].build(p.apvts, rtBandIDs[i], rtBandLbls[i], this, laf);
// -- ProMode: SatType --
satTypeLabel.setText("SAT TYPE", juce::dontSendNotification);
satTypeLabel.setFont(juce::Font(juce::FontOptions(9.f)));
satTypeLabel.setColour(juce::Label::textColourId, AmbivalenceColors::TextSecondary);
satTypeLabel.setJustificationType(juce::Justification::centred);
addAndMakeVisible(satTypeLabel);
satTypeCombo.addItemList({ "Warm","Tape","Tube","Hard" }, 1);
satTypeCombo.setLookAndFeel(&laf);
addAndMakeVisible(satTypeCombo);
satTypeAttachment.reset(
new juce::AudioProcessorValueTreeState::ComboBoxAttachment(
p.apvts, "sattype", satTypeCombo));
// -- ProMode: Tilt EQ + Output EQ --
BK(kTiltLow, "tiltlow", "TILT LOW");
BK(kTiltMid, "tiltmid", "TILT MID");
BK(kTiltHigh, "tilthigh", "TILT HIGH");
BK(kLoCutPro, "locut", "LO CUT");
BK(kHiCutPro, "hicut", "HI CUT");
// -------------------------------------------------------------------------
// preset UI
// -------------------------------------------------------------------------
presetManager = std::make_unique<PresetManager>(p);
// < PREV
presetPrevButton.setButtonText("<");
presetPrevButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface);
presetPrevButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::TextPrimary);
addAndMakeVisible(presetPrevButton);
presetPrevButton.onClick = [this] {
presetManager->loadPrevPreset();
};
// preset name combo
presetCombo.setLookAndFeel(&laf);
addAndMakeVisible(presetCombo);
presetCombo.onChange = [this] {
int idx = presetCombo.getSelectedItemIndex();
auto names = presetManager->getPresetNames();
if (idx >= 0 && idx < names.size())
presetManager->loadPreset(names[idx]);
};
// > NEXT
presetNextButton.setButtonText(">");
presetNextButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface);
presetNextButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::TextPrimary);
addAndMakeVisible(presetNextButton);
presetNextButton.onClick = [this] {
presetManager->loadNextPreset();
};
// SAVE
presetSaveButton.setButtonText("SAVE");
presetSaveButton.setColour(juce::TextButton::buttonColourId,
AmbivalenceColors::Accent.withAlpha(0.75f));
presetSaveButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::Background);
addAndMakeVisible(presetSaveButton);
presetSaveButton.onClick = [this] { savePresetWithDialog(); };
// --- after changes ---
// LOAD
presetLoadButton.setButtonText("LOAD");
presetLoadButton.setColour(juce::TextButton::buttonColourId,
AmbivalenceColors::AccentBlue.withAlpha(0.75f));
presetLoadButton.setColour(juce::TextButton::textColourOffId,
AmbivalenceColors::Background);
addAndMakeVisible(presetLoadButton);
presetLoadButton.onClick = [this] {
auto names = presetManager->getPresetNames();
int idx = presetCombo.getSelectedItemIndex();
if (idx >= 0 && idx < names.size())
presetManager->loadPreset(names[idx]);
};
// DELETE
presetDeleteButton.setButtonText("DELETE");
presetDeleteButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface);
presetDeleteButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::TextSecondary);
addAndMakeVisible(presetDeleteButton);
presetDeleteButton.onClick = [this] { deleteCurrentPreset(); };
// callback setup
// in the constructor callback setup
presetManager->onPresetListChanged = [this] { refreshPresetCombo(); };
// * fix : whenever the preset name changes Processor notify
presetManager->onPresetLoaded = [this](const juce::String& name) {
audioProcessor.setLastSavedPresetName(name);
refreshPresetCombo();
};
// -- Visualizers --
rt60Viz.setProcessor(&p);
decayCurveViz.setProcessor(&p);
addAndMakeVisible(rt60Viz);
addAndMakeVisible(decayCurveViz);
addAndMakeVisible(vuIn);
addAndMakeVisible(vuOut);
// -- AcousticMetrics --
labelMetricsTitle.setText("ACOUSTICS", juce::dontSendNotification);
labelMetricsTitle.setFont(juce::Font(juce::FontOptions(
"Helvetica Neue", 8.5f, juce::Font::bold)));
labelMetricsTitle.setColour(juce::Label::textColourId,
AmbivalenceColors::Accent.withAlpha(0.75f));
labelMetricsTitle.setJustificationType(juce::Justification::centredLeft);
addAndMakeVisible(labelMetricsTitle);
auto setupCaption = [this](juce::Label& label, const juce::String& text) {
label.setText(text, juce::dontSendNotification);
label.setFont(juce::Font(juce::FontOptions(
"Helvetica Neue", 8.0f, juce::Font::plain)));
label.setColour(juce::Label::textColourId,
AmbivalenceColors::TextSecondary.withAlpha(0.85f));
label.setJustificationType(juce::Justification::centredRight);
addAndMakeVisible(label);
};
auto setupValue = [this](juce::Label& label) {
label.setText("--", juce::dontSendNotification);
label.setFont(juce::Font(juce::FontOptions(
"Helvetica Neue", 9.5f, juce::Font::bold)));
label.setColour(juce::Label::textColourId, AmbivalenceColors::TextPrimary);
label.setJustificationType(juce::Justification::centredLeft);
addAndMakeVisible(label);
};
setupCaption(labelD50Caption, "D50:");
setupCaption(labelC50Caption, "C50:");
setupCaption(labelC80Caption, "C80:");
setupCaption(labelEDTCaption, "EDT:");
setupValue(labelD50Value);
setupValue(labelC50Value);
setupValue(labelC80Value);
setupValue(labelEDTValue);
// -- Git build info --
juce::String gitInfo = "Git: " + juce::String(AMBIVALENCE_GIT_BRANCH) + " @ " +
juce::String(AMBIVALENCE_GIT_COMMIT);
#if AMBIVALENCE_GIT_DIRTY
gitInfo += " (dirty)";
#endif
gitInfo += " | Build: " + juce::String(__DATE__) + " " + juce::String(__TIME__) + " (local)";
statusLabel.setText(gitInfo, juce::dontSendNotification);
statusLabel.setFont(juce::Font(juce::FontOptions(
"Helvetica Neue", 8.5f, juce::Font::plain)));
statusLabel.setColour(juce::Label::textColourId,
AmbivalenceColors::TextSecondary.withAlpha(0.6f));
statusLabel.setJustificationType(juce::Justification::centred);
addAndMakeVisible(statusLabel);
// --- after changes ---
refreshPresetCombo(); // added: initialize the combo at startup
updatePanelVisibility();
startTimerHz(60);
}
// -----------------------------------------------------------------------------
// destructor
// -----------------------------------------------------------------------------
FDNReverbEditor::~FDNReverbEditor() {
stopTimer();
setLookAndFeel(nullptr);
satTypeCombo.setLookAndFeel(nullptr);
presetCombo.setLookAndFeel(nullptr);
}
// -----------------------------------------------------------------------------
// timerCallback
// -----------------------------------------------------------------------------
void FDNReverbEditor::timerCallback()
{
vuIn.setLevels(audioProcessor.getInputRMSL(),
audioProcessor.getInputRMSR());
vuOut.setLevels(audioProcessor.getOutputRMSL(),
audioProcessor.getOutputRMSR());
vuIn.repaint();
vuOut.repaint();
static int metricsCounter = 0;
if (++metricsCounter >= 2) {
metricsCounter = 0;
labelD50Value.setText(
juce::String(audioProcessor.getD50() * 100.0f, 1) + "%",
juce::dontSendNotification);
labelC50Value.setText(
juce::String(audioProcessor.getC50(), 1) + "dB",
juce::dontSendNotification);
labelC80Value.setText(
juce::String(audioProcessor.getC80(), 1) + "dB",
juce::dontSendNotification);
labelEDTValue.setText(
juce::String(audioProcessor.getEDT(), 2) + "s",
juce::dontSendNotification);
}
bool newProMode = (*audioProcessor.apvts.getRawParameterValue("promode") > 0.5f);
if (newProMode != isProMode) {
isProMode = newProMode;
updatePanelVisibility();
resized();
repaint();
}
}
// -----------------------------------------------------------------------------
// updatePanelVisibility
// -----------------------------------------------------------------------------
void FDNReverbEditor::updatePanelVisibility()
{
auto setKnob = [](ArcKnob& k, bool vis) {
k.slider.setVisible(vis);
k.label.setVisible(vis);
};
const bool showNormal = !isProMode;
const bool showPro = isProMode;
setKnob(kPreDelay, showNormal);
setKnob(kRoomSize, showNormal);
setKnob(kDecay, showNormal);
setKnob(kHFDamp, showNormal);
setKnob(kLFAbsorb, showNormal);
setKnob(kDiffusion, showNormal);
setKnob(kModAmt, showNormal);
setKnob(kModRate, showNormal);
setKnob(kStereoW, showNormal);
setKnob(kERLevel, showNormal);
setKnob(kSaturation, showNormal);
setKnob(kWet, showNormal);
setKnob(kDry, showNormal);
setKnob(kDuckAmt, showNormal);
setKnob(kDuckThr, showNormal);
setKnob(kDuckAtt, showNormal);
setKnob(kDuckRel, showNormal);
setKnob(kLoCutNorm, showNormal);
setKnob(kHiCutNorm, showNormal);
for (auto& k : kRTBands) setKnob(k, showPro);
satTypeLabel.setVisible(showPro);
satTypeCombo.setVisible(showPro);
setKnob(kTiltLow, showPro);
setKnob(kTiltMid, showPro);
setKnob(kTiltHigh, showPro);
setKnob(kLoCutPro, showPro);
setKnob(kHiCutPro, showPro);
// preset UI always visible
presetPrevButton.setVisible(true);
presetCombo.setVisible(true);
presetNextButton.setVisible(true);
// --- after changes ---
presetSaveButton.setVisible(true);
presetLoadButton.setVisible(true); // * added
presetDeleteButton.setVisible(true);
}
// -----------------------------------------------------------------------------
// resized
// -----------------------------------------------------------------------------
void FDNReverbEditor::resized()
{
titleLabel.setBounds(PAD, Y_HEADER, 180, 32);
proModeButton.setBounds(196, Y_HEADER + 5, 52, 22);
erSoloButton.setBounds(256, Y_HEADER + 5, 72, 22);
vuIn.setBounds(W - 220, Y_HEADER + 2, 96, 28);
vuOut.setBounds(W - 120, Y_HEADER + 2, 96, 28);
algoSelector.setBounds(PAD, Y_ALGO, W - PAD * 2, 30);
auto place1 = [&](ArcKnob& k, int& x, int y) {
k.label.setBounds(x, y, KNOB_W, KNOB_LBL_H);
k.slider.setBounds(x, y + KNOB_LBL_H, KNOB_W, KNOB_H);
x += KNOB_W + ROW1_GAP;
};
auto place2 = [&](ArcKnob& k, int& x, int y) {
k.label.setBounds(x, y, KNOB_W, KNOB_LBL_H);
k.slider.setBounds(x, y + KNOB_LBL_H, KNOB_W, KNOB_H);
x += KNOB_W + PAD;
};
if (!isProMode) {
// -- Row 1 --
int kx = PAD;
place1(kPreDelay, kx, Y_ROW1);
place1(kRoomSize, kx, Y_ROW1);
place1(kDecay, kx, Y_ROW1);
place1(kHFDamp, kx, Y_ROW1);
place1(kLFAbsorb, kx, Y_ROW1);
place1(kDiffusion, kx, Y_ROW1);
place1(kModAmt, kx, Y_ROW1);
place1(kModRate, kx, Y_ROW1);
place1(kStereoW, kx, Y_ROW1);
place1(kERLevel, kx, Y_ROW1);
place1(kSaturation, kx, Y_ROW1);
// -- Row 2: MIX | OUT EQ | DUCKING --
kx = PAD;
place2(kWet, kx, Y_ROW2);
place2(kDry, kx, Y_ROW2);
kx += 16;
place2(kLoCutNorm, kx, Y_ROW2);
place2(kHiCutNorm, kx, Y_ROW2);
kx += 16;
place2(kDuckAmt, kx, Y_ROW2);
place2(kDuckThr, kx, Y_ROW2);
place2(kDuckAtt, kx, Y_ROW2);
place2(kDuckRel, kx, Y_ROW2);
}
else {
// -- ProMode row 1 --
int kx = PAD;
for (int i = 0; i < 10; ++i)
place1(kRTBands[i], kx, Y_ROW1);
// -- ProMode row 2 --
int kx2 = PAD;
satTypeLabel.setBounds(kx2, Y_SLABEL2, KNOB_W, KNOB_LBL_H);
satTypeCombo.setBounds(kx2, Y_SLABEL2 + KNOB_LBL_H + 2, KNOB_W + PAD, 24);
kx2 += KNOB_W + PAD + PAD + 8;
place2(kTiltLow, kx2, Y_ROW2);
place2(kTiltMid, kx2, Y_ROW2);
place2(kTiltHigh, kx2, Y_ROW2);
kx2 += 16;
place2(kLoCutPro, kx2, Y_ROW2);
place2(kHiCutPro, kx2, Y_ROW2);
}
// -------------------------------------------------------------------------
// preset UI (always, mode-independent)
// -------------------------------------------------------------------------
// layout (PRESET_PANEL_X=632 starting from ):
//
// top row Y=Y_ROW2: [<(26)] gap4 [combo(154)] gap4 [>(26)] -> right edge 848
// bottom row Y=Y_ROW2+34:[SAVE(104)] gap8 [DELETE(104)] -> right edge 848
//
// separator vertical line : PRESET_PANEL_X - 9 = 623
// -------------------------------------------------------------------------
{
const int px = PRESET_PANEL_X;
const int btnH = 26;
// top row
presetPrevButton.setBounds(px, Y_ROW2, 26, btnH);
presetCombo.setBounds(px + 30, Y_ROW2, 154, btnH);
presetNextButton.setBounds(px + 188, Y_ROW2, 26, btnH);
// --- after changes ---
// bottom row : [SAVE(68)] [LOAD(68)] [DELETE(68)]
presetSaveButton.setBounds(px, Y_ROW2 + 34, 68, btnH);
presetLoadButton.setBounds(px + 72, Y_ROW2 + 34, 68, btnH);
presetDeleteButton.setBounds(px + 144, Y_ROW2 + 34, 68, btnH);
}
// -- Visualizers --
const int vizTotalH = H - Y_VIZ - STATUS_H - PAD;
const int rt60Height = vizTotalH / 2 - 2;
const int decayHeight = vizTotalH / 2 - 2;
const int decayY = Y_VIZ + rt60Height + 4;
rt60Viz.setBounds(PAD, Y_VIZ, W - PAD * 2, rt60Height);
decayCurveViz.setBounds(PAD, decayY, W - PAD * 2, decayHeight);
// -- AcousticMetrics --
const int metricsRight = W - PAD - 8;
const int metricsW = 280;
const int metricsLeft = metricsRight - metricsW;
const int metricsTop = decayY + 6;
const int metricsRowH = 14;
const int metricsRow1Y = metricsTop + 14;
const int metricsRow2Y = metricsRow1Y + metricsRowH;
const int captionW = 28;
const int valueW = 52;
const int colSpacing = 8;
const int colW = captionW + valueW;
labelMetricsTitle.setBounds(metricsLeft, metricsTop, 80, 12);
labelD50Caption.setBounds(metricsLeft, metricsRow1Y, captionW, metricsRowH);
labelD50Value.setBounds(metricsLeft + captionW, metricsRow1Y, valueW, metricsRowH);
labelC50Caption.setBounds(metricsLeft + colW + colSpacing, metricsRow1Y, captionW, metricsRowH);
labelC50Value.setBounds(metricsLeft + colW + colSpacing + captionW, metricsRow1Y, valueW, metricsRowH);
labelC80Caption.setBounds(metricsLeft, metricsRow2Y, captionW, metricsRowH);
labelC80Value.setBounds(metricsLeft + captionW, metricsRow2Y, valueW, metricsRowH);
labelEDTCaption.setBounds(metricsLeft + colW + colSpacing, metricsRow2Y, captionW, metricsRowH);
labelEDTValue.setBounds(metricsLeft + colW + colSpacing + captionW, metricsRow2Y, valueW, metricsRowH);
// -- Git build-info status bar --
statusLabel.setBounds(PAD, H - STATUS_H, W - PAD * 2, STATUS_H);
}
// -----------------------------------------------------------------------------
// paint
// -----------------------------------------------------------------------------
void FDNReverbEditor::paint(juce::Graphics& g)
{
g.fillAll(AmbivalenceColors::Background);
juce::ColourGradient grad(
AmbivalenceColors::Surface.withAlpha(0.12f), 0.f, 0.f,
AmbivalenceColors::Background, 0.f, (float)H, false);
g.setGradientFill(grad);
g.fillAll();
g.setFont(juce::Font(juce::FontOptions(8.f)));
g.setColour(AmbivalenceColors::TextSecondary.withAlpha(0.35f));
g.drawText("16ch FDN | SAPF | ISM-ER | 44.1-192kHz",
PAD + 336, Y_HEADER + 10, W / 2, 12,
juce::Justification::centredLeft);
g.setColour(AmbivalenceColors::Separator);
g.drawHorizontalLine(Y_SEP, (float)PAD, (float)(W - PAD));
g.setFont(juce::Font(juce::FontOptions(
"Helvetica Neue", 8.5f, juce::Font::bold)));
auto sl = [&](int x, int y, const char* t) {
g.drawText(t, x, y, 200, 14, juce::Justification::centredLeft);
};
if (!isProMode) {
// -- Row 1 separator --
g.setColour(AmbivalenceColors::Separator);
g.drawVerticalLine(SEP_TF, (float)Y_SLABEL1, (float)(Y_ROW1 + UNIT_H));
g.drawVerticalLine(SEP_FD, (float)Y_SLABEL1, (float)(Y_ROW1 + UNIT_H));
g.drawVerticalLine(SEP_DS, (float)Y_SLABEL1, (float)(Y_ROW1 + UNIT_H));
g.drawVerticalLine(SEP_SC, (float)Y_SLABEL1, (float)(Y_ROW1 + UNIT_H));
// -- Row 2 separator --
const int row2_outeq_x = PAD + 2 * (KNOB_W + PAD) + 16;
const int row2_duck_x = row2_outeq_x + 2 * (KNOB_W + PAD) + 16;
g.drawVerticalLine(row2_outeq_x - 9, (float)Y_SLABEL2, (float)(Y_ROW2 + UNIT_H));
g.drawVerticalLine(row2_duck_x - 9, (float)Y_SLABEL2, (float)(Y_ROW2 + UNIT_H));
// -- section --
g.setColour(AmbivalenceColors::Accent.withAlpha(0.75f));
sl(SEC_TIME, Y_SLABEL1, "TIME");
sl(SEC_FREQUENCY, Y_SLABEL1, "FREQUENCY");
sl(SEC_DIFFUSION, Y_SLABEL1, "DIFFUSION");
sl(SEC_STEREO, Y_SLABEL1, "STEREO");
sl(SEC_CHARACTER, Y_SLABEL1, "CHARACTER");
sl(PAD, Y_SLABEL2, "MIX");
sl(row2_outeq_x, Y_SLABEL2, "OUT EQ");
sl(row2_duck_x, Y_SLABEL2, "DUCKING");
}
else {
// -- ProMode --
g.setColour(AmbivalenceColors::Accent.withAlpha(0.75f));
sl(PAD, Y_SLABEL1, "RT60 PER BAND");
g.setColour(AmbivalenceColors::Separator.withAlpha(0.5f));
g.drawHorizontalLine(Y_SLABEL2 - 4, (float)PAD, (float)(W - PAD));
const int tilt_x = PAD + KNOB_W + PAD + PAD + 8;
const int outeq_x = tilt_x + 3 * (KNOB_W + PAD) + 16;
g.setColour(AmbivalenceColors::Separator);
g.drawVerticalLine(outeq_x - 9, (float)Y_SLABEL2, (float)(Y_ROW2 + UNIT_H));
g.setColour(AmbivalenceColors::Accent.withAlpha(0.75f));
sl(tilt_x, Y_SLABEL2, "TILT EQ");
sl(outeq_x, Y_SLABEL2, "OUT EQ");
}
// -------------------------------------------------------------------------
// preset section (always, mode-independent)
// -------------------------------------------------------------------------
g.setColour(AmbivalenceColors::Separator);
g.drawVerticalLine(PRESET_PANEL_X - 9,
(float)Y_SLABEL2, (float)(Y_ROW2 + UNIT_H));
g.setColour(AmbivalenceColors::Accent.withAlpha(0.75f));
sl(PRESET_PANEL_X, Y_SLABEL2, "PRESET");
// -- Git build-info status bar divider --
g.setColour(AmbivalenceColors::Separator);
g.drawHorizontalLine(H - STATUS_H - 1, (float)PAD, (float)(W - PAD));
}
// -----------------------------------------------------------------------------
// Preset UI helpers
// -----------------------------------------------------------------------------
void FDNReverbEditor::refreshPresetCombo()
{
presetCombo.clear(juce::dontSendNotification);
auto names = presetManager->getPresetNames();
// ---------------------------------------------------------------------
// * fix: restore the preset name saved in the Processor at startup
// ---------------------------------------------------------------------
// when the editor is closed the PresetManager is destroyed,
// so currentPresetName becomes empty.
// the name persisted in the Processor as lastSavedPresetName is
// re-set in the new PresetManager,
// so the combo box selection is restored correctly.
// ---------------------------------------------------------------------
if (presetManager->getCurrentPresetName().isEmpty()) {
auto saved = audioProcessor.getLastSavedPresetName();
if (saved.isNotEmpty())
presetManager->setCurrentPresetName(saved);
}
if (names.isEmpty()) {
presetCombo.addItem("-- No Presets --", 1);
presetCombo.setSelectedItemIndex(0, juce::dontSendNotification);
presetDeleteButton.setEnabled(false);
presetLoadButton.setEnabled(false);
presetPrevButton.setEnabled(false);
presetNextButton.setEnabled(false);
return;
}
for (int i = 0; i < names.size(); ++i)
presetCombo.addItem(names[i], i + 1);
int idx = presetManager->getCurrentPresetIndex();
if (idx >= 0)
presetCombo.setSelectedItemIndex(idx, juce::dontSendNotification);
else
presetCombo.setSelectedItemIndex(0, juce::dontSendNotification);
presetDeleteButton.setEnabled(true);
presetLoadButton.setEnabled(true);
presetPrevButton.setEnabled(names.size() > 1);
presetNextButton.setEnabled(names.size() > 1);
}
void FDNReverbEditor::savePresetWithDialog()
{
// ---------------------------------------------------------------------
// AlertWindow preset name input dialog
// uses enterModalState(async callback): no runModalLoop() needed
// SafePointer handles the case where the editor is destroyed first
// ---------------------------------------------------------------------
auto* dialog = new juce::AlertWindow(
"Save Preset",
"Enter a name for this preset:",
juce::MessageBoxIconType::NoIcon);
dialog->addTextEditor("name", presetManager->getCurrentPresetName());
dialog->addButton("Save", 1, juce::KeyPress(juce::KeyPress::returnKey));
dialog->addButton("Cancel", 0, juce::KeyPress(juce::KeyPress::escapeKey));
juce::Component::SafePointer<FDNReverbEditor> safeThis(this);
dialog->enterModalState(
true,
juce::ModalCallbackFunction::create(
[safeThis, dialog](int result) {
if (safeThis != nullptr && result == 1) {
auto name = dialog->getTextEditorContents("name").trim();
if (name.isNotEmpty())
safeThis->presetManager->savePreset(name);
}
}),
true // deleteWhenDismissed
);
}
void FDNReverbEditor::deleteCurrentPreset()
{
auto name = presetManager->getCurrentPresetName();
if (name.isEmpty()) return;
auto* dialog = new juce::AlertWindow(
"Delete Preset",
"Delete \"" + name + "\"?",
juce::MessageBoxIconType::WarningIcon);
dialog->addButton("Delete", 1);
dialog->addButton("Cancel", 0, juce::KeyPress(juce::KeyPress::escapeKey));
juce::Component::SafePointer<FDNReverbEditor> safeThis(this);
dialog->enterModalState(
true,
juce::ModalCallbackFunction::create(
[safeThis, name](int result) {
if (safeThis != nullptr && result == 1)
safeThis->presetManager->deletePreset(name);
}),
true
);
}

97
Source/PluginEditor.h Normal file
View file

@ -0,0 +1,97 @@
#pragma once
#include <JuceHeader.h>
#include "PluginProcessor.h"
#include "PresetManager.h"
#include "GUI/AmbivalenceUI.h"
#include "GUI/DecayCurveViz.h"
class FDNReverbEditor : public juce::AudioProcessorEditor,
private juce::Timer
{
public:
explicit FDNReverbEditor(FDNReverbAudioProcessor&);
~FDNReverbEditor() override;
void paint(juce::Graphics&) override;
void resized() override;
private:
void timerCallback() override;
void updatePanelVisibility();
// --- Preset UI helpers ---
void refreshPresetCombo();
void savePresetWithDialog();
void deleteCurrentPreset();
FDNReverbAudioProcessor& audioProcessor;
AmbivalenceLookAndFeel laf;
// --- common ---
AlgorithmSelector algoSelector;
RT60Visualizer rt60Viz;
DecayCurveViz decayCurveViz;
VUMeter vuIn, vuOut;
juce::Label titleLabel;
juce::Label labelMetricsTitle;
juce::Label labelD50Caption, labelD50Value;
juce::Label labelC50Caption, labelC50Value;
juce::Label labelC80Caption, labelC80Value;
juce::Label labelEDTCaption, labelEDTValue;
juce::Label statusLabel;
juce::TextButton proModeButton;
juce::TextButton erSoloButton;
std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> proModeAttachment;
std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> erSoloAttachment;
bool isProMode{ false };
// --- Normal Mode ---
ArcKnob kPreDelay, kRoomSize, kDecay;
ArcKnob kHFDamp, kLFAbsorb;
ArcKnob kDiffusion, kModAmt, kModRate;
ArcKnob kStereoW;
ArcKnob kERLevel, kSaturation;
ArcKnob kWet, kDry;
ArcKnob kDuckAmt, kDuckThr, kDuckAtt, kDuckRel;
ArcKnob kLoCutNorm, kHiCutNorm;
// --- ProMode panel ---
std::array<ArcKnob, 10> kRTBands;
juce::Label satTypeLabel;
juce::ComboBox satTypeCombo;
std::unique_ptr<juce::AudioProcessorValueTreeState::ComboBoxAttachment> satTypeAttachment;
ArcKnob kTiltLow, kTiltMid, kTiltHigh;
ArcKnob kLoCutPro, kHiCutPro;
// --- preset UI ---
std::unique_ptr<PresetManager> presetManager;
juce::TextButton presetPrevButton;
juce::ComboBox presetCombo;
juce::TextButton presetNextButton;
// existing presetSaveButton / presetDeleteButton next to added
juce::TextButton presetSaveButton;
juce::TextButton presetLoadButton; // * added
juce::TextButton presetDeleteButton;
// --- layout constants ---
static constexpr int W = 900;
static constexpr int H = 540;
static constexpr int PAD = 8;
static constexpr int KNOB_W = 64;
static constexpr int KNOB_H = 72;
static constexpr int KNOB_LBL_H = 14;
static constexpr int UNIT_H = 88;
static constexpr int ROW1_GAP = 18;
static constexpr int STATUS_H = 16;
// Fixed X of the preset panel
// DUCKING end (616) + gap(16) = 632
static constexpr int PRESET_PANEL_X = 632;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(FDNReverbEditor)
};

View file

@ -0,0 +1,87 @@
#include "PluginParameters.h"
namespace FDNReverb {
juce::AudioProcessorValueTreeState::ParameterLayout ParameterHelper::createLayout()
{
std::vector<std::unique_ptr<juce::RangedAudioParameter>> params;
auto addFloat = [&](const juce::String& id,
const juce::String& name,
float min, float max, float def,
float skew = 1.0f,
const juce::String& label = "")
{
params.push_back(std::make_unique<juce::AudioParameterFloat>(
id, name,
juce::NormalisableRange<float>(min, max, 0.01f, skew),
def,
juce::AudioParameterFloatAttributes().withLabel(label)));
};
params.push_back(std::make_unique<juce::AudioParameterChoice>(
ParamID::Algorithm, "Algorithm",
juce::StringArray{ "ROOM1","ROOM2","HALL1","HALL2","PLATE","SPRING","GOLDFOIL" }, 0));
addFloat(ParamID::PreDelay, "Pre-Delay", 0.0f, 500.0f, 10.0f, 1.0f, "ms");
addFloat(ParamID::RoomSize, "Room Size", 0.3f, 2.0f, 1.0f);
addFloat(ParamID::DecayTime, "Decay Time", 0.1f, 20.0f, 1.5f, 0.35f, "s");
addFloat(ParamID::HFDamping, "HF Damping", 0.0f, 1.0f, 0.0f);
addFloat(ParamID::LFAbsorption, "LF Absorption", 0.0f, 1.0f, 0.0f);
addFloat(ParamID::Diffusion, "Diffusion", 0.0f, 1.0f, 0.7f);
addFloat(ParamID::ModAmount, "Mod Amount", 0.0f, 1.0f, 0.25f);
addFloat(ParamID::ModRate, "Mod Rate", 0.05f, 2.0f, 0.5f, 1.0f, "Hz");
addFloat(ParamID::StereoWidth, "Stereo Width", 0.0f, 1.0f, 0.8f);
addFloat(ParamID::ERLevel, "ER Level", 0.0f, 1.0f, 0.6f);
addFloat(ParamID::Saturation, "Saturation", 0.0f, 1.0f, 0.0f);
params.push_back(std::make_unique<juce::AudioParameterChoice>(
ParamID::SatType, "Sat Type",
juce::StringArray{ "Warm","Tape","Tube","Hard" },
0,
juce::AudioParameterChoiceAttributes().withAutomatable(false)));
// * Step B: Wet -6dB / Dry 0dB change
// internal offset -3dB (PluginProcessor.cpp)
// effective Wet displayed value -3dB .
// Wet=-6dB -> effective -9dB, Wet=0dB -> effective -3dB
addFloat(ParamID::WetLevel, "Wet", -60.0f, 0.0f, -4.0f, 1.0f, "dB");
addFloat(ParamID::DryLevel, "Dry", -60.0f, 0.0f, 0.0f, 1.0f, "dB");
addFloat(ParamID::DuckAmount, "Ducking", 0.0f, 20.0f, 0.0f, 1.0f, "dB");
addFloat(ParamID::DuckAttack, "Duck Attack", 0.5f, 100.0f, 10.0f, 0.4f, "ms");
addFloat(ParamID::DuckRelease, "Duck Release", 10.0f, 2000.0f, 200.0f, 0.4f, "ms");
addFloat(ParamID::DuckThresh, "Duck Thresh", -60.0f, 0.0f, -20.0f, 1.0f, "dB");
params.push_back(std::make_unique<juce::AudioParameterBool>(
ParamID::ERSolo, "ER Solo", false,
juce::AudioParameterBoolAttributes().withAutomatable(false)));
params.push_back(std::make_unique<juce::AudioParameterBool>(
ParamID::ProMode, "Pro Mode", false,
juce::AudioParameterBoolAttributes().withAutomatable(false)));
addFloat(ParamID::TiltLow, "Tilt Low", 0.5f, 2.0f, 1.0f);
addFloat(ParamID::TiltMid, "Tilt Mid", 0.5f, 2.0f, 1.0f);
addFloat(ParamID::TiltHigh, "Tilt High", 0.5f, 2.0f, 1.0f);
addFloat(ParamID::RTBand0, "RT 31Hz", 0.5f, 2.0f, 1.0f);
addFloat(ParamID::RTBand1, "RT 62Hz", 0.5f, 2.0f, 1.0f);
addFloat(ParamID::RTBand2, "RT 125Hz", 0.5f, 2.0f, 1.0f);
addFloat(ParamID::RTBand3, "RT 250Hz", 0.5f, 2.0f, 1.0f);
addFloat(ParamID::RTBand4, "RT 500Hz", 0.5f, 2.0f, 1.0f);
addFloat(ParamID::RTBand5, "RT 1kHz", 0.5f, 2.0f, 1.0f);
addFloat(ParamID::RTBand6, "RT 2kHz", 0.5f, 2.0f, 1.0f);
addFloat(ParamID::RTBand7, "RT 4kHz", 0.5f, 2.0f, 1.0f);
addFloat(ParamID::RTBand8, "RT 8kHz", 0.5f, 2.0f, 1.0f);
addFloat(ParamID::RTBand9, "RT 16kHz", 0.5f, 2.0f, 1.0f);
addFloat(ParamID::LoCut, "Lo Cut", 20.0f, 500.0f, 20.0f, 0.3f, "Hz");
addFloat(ParamID::HiCut, "Hi Cut", 1000.0f, 20000.0f, 20000.0f, 0.3f, "Hz");
return { params.begin(), params.end() };
}
} // namespace FDNReverb

117
Source/PluginParameters.h Normal file
View file

@ -0,0 +1,117 @@
#pragma once
#include <JuceHeader.h>
#include <array>
namespace FDNReverb {
namespace ParamID {
inline const juce::String Algorithm = "algorithm";
inline const juce::String PreDelay = "predelay";
inline const juce::String RoomSize = "roomsize";
inline const juce::String DecayTime = "decaytime";
inline const juce::String HFDamping = "hfdamping";
inline const juce::String LFAbsorption = "lfabsorption";
inline const juce::String Diffusion = "diffusion";
inline const juce::String ModAmount = "modamount";
inline const juce::String ModRate = "modrate";
inline const juce::String StereoWidth = "stereowidth";
inline const juce::String ERLevel = "erlevel";
inline const juce::String Saturation = "saturation";
inline const juce::String SatType = "sattype";
inline const juce::String WetLevel = "wetlevel";
inline const juce::String DryLevel = "drylevel";
inline const juce::String DuckAmount = "duckamount";
inline const juce::String DuckAttack = "duckattack";
inline const juce::String DuckRelease = "duckrelease";
inline const juce::String DuckThresh = "duckthresh";
inline const juce::String ERSolo = "ersolo";
inline const juce::String ProMode = "promode";
inline const juce::String TiltLow = "tiltlow";
inline const juce::String TiltMid = "tiltmid";
inline const juce::String TiltHigh = "tilthigh";
inline const juce::String RTBand0 = "rtband0";
inline const juce::String RTBand1 = "rtband1";
inline const juce::String RTBand2 = "rtband2";
inline const juce::String RTBand3 = "rtband3";
inline const juce::String RTBand4 = "rtband4";
inline const juce::String RTBand5 = "rtband5";
inline const juce::String RTBand6 = "rtband6";
inline const juce::String RTBand7 = "rtband7";
inline const juce::String RTBand8 = "rtband8";
inline const juce::String RTBand9 = "rtband9";
inline const juce::String LoCut = "locut";
inline const juce::String HiCut = "hicut";
}
struct DSPParams {
int algorithmIndex{ 0 };
float decayScale{ 1.0f };
float roomSizeScale{ 1.0f };
float hfDamping{ 0.0f };
float lfAbsorption{ 0.0f };
float diffusion{ 0.70f };
float preDelayMs{ 10.0f };
float modAmount{ 0.25f };
float modRate{ 0.5f };
float stereoWidth{ 0.80f };
float erLevel{ 0.6f };
float lateLevel{ 1.0f };
// * Step B: Wet -4dB / Dry 0dB
float wetDB{ -4.0f };
float dryDB{ 0.0f };
float saturation{ 0.0f };
int satTypeIdx{ 0 };
float duckingAmount{ 0.0f };
float duckingAttackMs{ 10.0f };
float duckingRelMs{ 200.0f };
float duckingThreshDB{ -20.0f };
bool erSolo{ false };
bool proMode{ false };
float tiltLow{ 1.0f };
float tiltMid{ 1.0f };
float tiltHigh{ 1.0f };
std::array<float, 10> rtBands{ { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f, 1.0f, 1.0f } };
float loCutHz{ 20.0f };
float hiCutHz{ 20000.0f };
bool operator==(const DSPParams& o) const noexcept {
return algorithmIndex == o.algorithmIndex
&& decayScale == o.decayScale
&& roomSizeScale == o.roomSizeScale
&& hfDamping == o.hfDamping
&& lfAbsorption == o.lfAbsorption
&& diffusion == o.diffusion
&& preDelayMs == o.preDelayMs
&& modAmount == o.modAmount
&& modRate == o.modRate
&& stereoWidth == o.stereoWidth
&& erLevel == o.erLevel
&& lateLevel == o.lateLevel
&& wetDB == o.wetDB
&& dryDB == o.dryDB
&& saturation == o.saturation
&& satTypeIdx == o.satTypeIdx
&& duckingAmount == o.duckingAmount
&& duckingAttackMs == o.duckingAttackMs
&& duckingRelMs == o.duckingRelMs
&& duckingThreshDB == o.duckingThreshDB
&& erSolo == o.erSolo
&& proMode == o.proMode
&& tiltLow == o.tiltLow
&& tiltMid == o.tiltMid
&& tiltHigh == o.tiltHigh
&& rtBands == o.rtBands
&& loCutHz == o.loCutHz
&& hiCutHz == o.hiCutHz;
}
bool operator!=(const DSPParams& o) const noexcept { return !(*this == o); }
};
class ParameterHelper {
public:
static juce::AudioProcessorValueTreeState::ParameterLayout createLayout();
};
} // namespace FDNReverb

223
Source/PluginProcessor.cpp Normal file
View file

@ -0,0 +1,223 @@
#include "PluginProcessor.h"
#include "PluginEditor.h"
using namespace FDNReverb;
// -----------------------------------------------------------------------------
// * Step A: Wet internal offset
// -----------------------------------------------------------------------------
// the UI shows -60 to 0 dB, but the effective Wet maximum is -3 dB.
// reason: at Wet=0 dB, combined with the FDN makeup gain the OutputLimiter
// would engage constantly and clip. -3 dB of headroom is required.
// implementation: rather than adding -3 dB (= 0.708x) to the APVTS value,
// multiply after Decibels::decibelsToGain() - numerically safer.
// -----------------------------------------------------------------------------
static constexpr float kWetInternalOffsetDB = -1.0f;
FDNReverbAudioProcessor::FDNReverbAudioProcessor()
: AudioProcessor(BusesProperties()
.withInput("Input", juce::AudioChannelSet::stereo(), true)
.withOutput("Output", juce::AudioChannelSet::stereo(), true)),
apvts(*this, nullptr, "FDNReverbState", ParameterHelper::createLayout())
{
}
void FDNReverbAudioProcessor::prepareToPlay(double sampleRate, int samplesPerBlock)
{
int osIdx = 0;
oversampler = std::make_unique<juce::dsp::Oversampling<float>>(
2, osIdx,
juce::dsp::Oversampling<float>::filterHalfBandPolyphaseIIR, true);
oversampler->initProcessing(static_cast<size_t>(samplesPerBlock));
engine.prepare(sampleRate, samplesPerBlock);
wetBuffer.setSize(2, samplesPerBlock);
smoothWetGain.reset(sampleRate, 0.05);
smoothDryGain.reset(sampleRate, 0.05);
lastSampleRate = sampleRate;
paramsNeedUpdate = true;
}
void FDNReverbAudioProcessor::updateEngineParams()
{
int currentAlgo = (int)*apvts.getRawParameterValue(ParamID::Algorithm);
if (currentAlgo != lastAlgorithmIndex) {
if (lastAlgorithmIndex >= 0)
loadPresetDefaults(currentAlgo);
lastAlgorithmIndex = currentAlgo;
paramsNeedUpdate = true;
}
DSPParams p;
p.algorithmIndex = (int)*apvts.getRawParameterValue(ParamID::Algorithm);
p.preDelayMs = *apvts.getRawParameterValue(ParamID::PreDelay);
p.roomSizeScale = *apvts.getRawParameterValue(ParamID::RoomSize) - 0.5f;
p.decayScale = *apvts.getRawParameterValue(ParamID::DecayTime)
/ ALL_PRESETS[p.algorithmIndex]->acoustics.rt60[4];
p.hfDamping = *apvts.getRawParameterValue(ParamID::HFDamping);
p.lfAbsorption = *apvts.getRawParameterValue(ParamID::LFAbsorption);
p.diffusion = *apvts.getRawParameterValue(ParamID::Diffusion);
p.modAmount = *apvts.getRawParameterValue(ParamID::ModAmount);
p.modRate = *apvts.getRawParameterValue(ParamID::ModRate);
p.stereoWidth = *apvts.getRawParameterValue(ParamID::StereoWidth);
p.erLevel = *apvts.getRawParameterValue(ParamID::ERLevel);
p.saturation = *apvts.getRawParameterValue(ParamID::Saturation);
p.wetDB = *apvts.getRawParameterValue(ParamID::WetLevel);
p.dryDB = *apvts.getRawParameterValue(ParamID::DryLevel);
p.duckingAmount = *apvts.getRawParameterValue(ParamID::DuckAmount);
p.duckingAttackMs = *apvts.getRawParameterValue(ParamID::DuckAttack);
p.duckingRelMs = *apvts.getRawParameterValue(ParamID::DuckRelease);
p.duckingThreshDB = *apvts.getRawParameterValue(ParamID::DuckThresh);
p.satTypeIdx = (int)*apvts.getRawParameterValue(ParamID::SatType);
p.erSolo = (*apvts.getRawParameterValue(ParamID::ERSolo)) > 0.5f;
p.proMode = (*apvts.getRawParameterValue(ParamID::ProMode)) > 0.5f;
p.tiltLow = *apvts.getRawParameterValue(ParamID::TiltLow);
p.tiltMid = *apvts.getRawParameterValue(ParamID::TiltMid);
p.tiltHigh = *apvts.getRawParameterValue(ParamID::TiltHigh);
p.rtBands[0] = *apvts.getRawParameterValue(ParamID::RTBand0);
p.rtBands[1] = *apvts.getRawParameterValue(ParamID::RTBand1);
p.rtBands[2] = *apvts.getRawParameterValue(ParamID::RTBand2);
p.rtBands[3] = *apvts.getRawParameterValue(ParamID::RTBand3);
p.rtBands[4] = *apvts.getRawParameterValue(ParamID::RTBand4);
p.rtBands[5] = *apvts.getRawParameterValue(ParamID::RTBand5);
p.rtBands[6] = *apvts.getRawParameterValue(ParamID::RTBand6);
p.rtBands[7] = *apvts.getRawParameterValue(ParamID::RTBand7);
p.rtBands[8] = *apvts.getRawParameterValue(ParamID::RTBand8);
p.rtBands[9] = *apvts.getRawParameterValue(ParamID::RTBand9);
p.loCutHz = *apvts.getRawParameterValue(ParamID::LoCut);
p.hiCutHz = *apvts.getRawParameterValue(ParamID::HiCut);
// * Step A: Wet internal -3dB offset apply
// -60~0dB, effective value -63~-3dB .
smoothWetGain.setTargetValue(
juce::Decibels::decibelsToGain(p.wetDB + kWetInternalOffsetDB));
smoothDryGain.setTargetValue(juce::Decibels::decibelsToGain(p.dryDB));
if (paramsNeedUpdate || p != lastSentParams) {
engine.setParams(p);
lastSentParams = p;
paramsNeedUpdate = false;
}
}
void FDNReverbAudioProcessor::processBlock(
juce::AudioBuffer<float>& buffer, juce::MidiBuffer&)
{
juce::ScopedNoDenormals noDenormals;
updateEngineParams();
inputRMS_L.store(buffer.getRMSLevel(0, 0, buffer.getNumSamples()));
inputRMS_R.store(buffer.getRMSLevel(1, 0, buffer.getNumSamples()));
juce::dsp::AudioBlock<float> block(buffer);
auto osBlock = oversampler->processSamplesUp(block);
int numSamples = static_cast<int>(osBlock.getNumSamples());
wetBuffer.setSize(2, numSamples, false, false, true);
engine.processBlock(osBlock.getChannelPointer(0), osBlock.getChannelPointer(1),
wetBuffer.getWritePointer(0), wetBuffer.getWritePointer(1),
numSamples);
for (int i = 0; i < numSamples; ++i) {
float w = smoothWetGain.getNextValue();
float d = smoothDryGain.getNextValue();
osBlock.setSample(0, i, osBlock.getSample(0, i) * d
+ wetBuffer.getSample(0, i) * w);
osBlock.setSample(1, i, osBlock.getSample(1, i) * d
+ wetBuffer.getSample(1, i) * w);
}
oversampler->processSamplesDown(block);
outputRMS_L.store(buffer.getRMSLevel(0, 0, buffer.getNumSamples()));
outputRMS_R.store(buffer.getRMSLevel(1, 0, buffer.getNumSamples()));
}
void FDNReverbAudioProcessor::getStateInformation(juce::MemoryBlock& d) {
auto state = apvts.copyState();
// * fix: save the current preset name in the ValueTree
// when the editor exists, get the name from the PresetManager
// the editor is not held directly by the AudioProcessor,
// so a preset-name field managed by the Processor was added.
if (lastSavedPresetName.isNotEmpty())
state.setProperty("currentPresetName", lastSavedPresetName, nullptr);
std::unique_ptr<juce::XmlElement> xml(state.createXml());
copyXmlToBinary(*xml, d);
}
void FDNReverbAudioProcessor::setStateInformation(const void* d, int s) {
std::unique_ptr<juce::XmlElement> xml(getXmlFromBinary(d, s));
if (xml && xml->hasTagName(apvts.state.getType())) {
auto tree = juce::ValueTree::fromXml(*xml);
// * fix : preset name restore
lastSavedPresetName = tree.getProperty("currentPresetName", "").toString();
apvts.replaceState(tree);
paramsNeedUpdate = true;
}
}
juce::AudioProcessorEditor* FDNReverbAudioProcessor::createEditor() {
return new FDNReverbEditor(*this);
}
void FDNReverbAudioProcessor::loadPresetDefaults(int algorithmIndex)
{
if (algorithmIndex < 0 || algorithmIndex >= 7) return;
const auto& def = PRESET_DEFAULTS[algorithmIndex];
auto setParam = [this](const juce::String& paramID, float value) {
if (auto* param = apvts.getParameter(paramID)) {
param->setValueNotifyingHost(param->convertTo0to1(value));
}
};
setParam(ParamID::RoomSize, def.roomSize);
setParam(ParamID::DecayTime, def.decayTime);
// * Step A: HF Damping / LF Absorption always 0
// AlgorithmPresets.h def.hfDamp / def.lfAbsorb .
// reason : after " preset RT60 curve "
// correct . intentional correction
// before correction .
setParam(ParamID::HFDamping, 0.0f);
setParam(ParamID::LFAbsorption, 0.0f);
setParam(ParamID::Diffusion, def.diffusion);
setParam(ParamID::ModAmount, def.modAmount);
setParam(ParamID::ModRate, def.modRate);
setParam(ParamID::ERLevel, def.erLevel);
setParam(ParamID::Saturation, def.saturation);
setParam(ParamID::RTBand0, 1.0f);
setParam(ParamID::RTBand1, 1.0f);
setParam(ParamID::RTBand2, 1.0f);
setParam(ParamID::RTBand3, 1.0f);
setParam(ParamID::RTBand4, 1.0f);
setParam(ParamID::RTBand5, 1.0f);
setParam(ParamID::RTBand6, 1.0f);
setParam(ParamID::RTBand7, 1.0f);
setParam(ParamID::RTBand8, 1.0f);
setParam(ParamID::RTBand9, 1.0f);
setParam(ParamID::TiltLow, 1.0f);
setParam(ParamID::TiltMid, 1.0f);
setParam(ParamID::TiltHigh, 1.0f);
paramsNeedUpdate = true;
}
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() {
return new FDNReverbAudioProcessor();
}

91
Source/PluginProcessor.h Normal file
View file

@ -0,0 +1,91 @@
#pragma once
#include <JuceHeader.h>
#include "DSP/UniversalEngine.h"
#include "PluginParameters.h"
class FDNReverbAudioProcessor : public juce::AudioProcessor
{
public:
FDNReverbAudioProcessor();
void prepareToPlay(double sampleRate, int samplesPerBlock) override;
void releaseResources() override { engine.reset(); }
void processBlock(juce::AudioBuffer<float>&, juce::MidiBuffer&) override;
void processBlockBypassed(juce::AudioBuffer<float>&, juce::MidiBuffer&) override {}
juce::AudioProcessorEditor* createEditor() override;
bool hasEditor() const override { return true; }
const juce::String getName() const override { return "Ambivalence1.1"; }
double getTailLengthSeconds() const override { return 20.0; }
bool acceptsMidi() const override { return false; }
bool producesMidi() const override { return false; }
bool isMidiEffect() const override { return false; }
int getNumPrograms() override { return 1; }
int getCurrentProgram() override { return 0; }
void setCurrentProgram(int) override {}
const juce::String getProgramName(int) override { return {}; }
void changeProgramName(int, const juce::String&) override {}
void getStateInformation(juce::MemoryBlock& destData) override;
void setStateInformation(const void* data, int sizeInBytes) override;
juce::AudioProcessorValueTreeState apvts;
std::array<float, FDNReverb::NUM_BANDS> getRT60ForDisplay() const noexcept {
return engine.getEffectiveRT60();
}
float getInputRMSL() const noexcept { return inputRMS_L.load(); }
float getInputRMSR() const noexcept { return inputRMS_R.load(); }
float getOutputRMSL() const noexcept { return outputRMS_L.load(); }
float getOutputRMSR() const noexcept { return outputRMS_R.load(); }
float getD50() const noexcept { return engine.getD50(); }
float getC50() const noexcept { return engine.getC50(); }
float getC80() const noexcept { return engine.getC80(); }
float getEDT() const noexcept { return engine.getEDT(); }
const FDNReverb::UniversalEngine& getEngine() const noexcept { return engine; }
void loadPresetDefaults(int algorithmIndex);
private:
void updateEngineParams();
FDNReverb::UniversalEngine engine;
// --- dirty flag: skip setParams() when no parameter changed ---
// processBlock() calls updateEngineParams() every buffer,
// but the designStage2() x 16 WLS computation is skipped when nothing changed
FDNReverb::DSPParams lastSentParams;
bool paramsNeedUpdate{ true }; // the first run
int lastAlgorithmIndex{ -1 };
std::unique_ptr<juce::dsp::Oversampling<float>> oversampler;
juce::AudioBuffer<float> wetBuffer;
juce::SmoothedValue<float> smoothWetGain, smoothDryGain;
std::atomic<float> inputRMS_L{ 0.f }, inputRMS_R{ 0.f };
std::atomic<float> outputRMS_L{ 0.f }, outputRMS_R{ 0.f };
double lastSampleRate{ 0.0 };
// * added : for session saving preset name
// PresetManager editor ,
// Processor preset name save support .
juce::String lastSavedPresetName;
public:
// editor call preset name Processor notify
void setLastSavedPresetName(const juce::String& name) noexcept {
lastSavedPresetName = name;
}
juce::String getLastSavedPresetName() const noexcept {
return lastSavedPresetName;
}
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(FDNReverbAudioProcessor)
};

134
Source/PresetManager.cpp Normal file
View file

@ -0,0 +1,134 @@
#include "PresetManager.h"
#include "PluginProcessor.h"
PresetManager::PresetManager(FDNReverbAudioProcessor& p)
: processor(p)
{
refreshPresetList();
}
// -----------------------------------------------------------------------------
// filesystem
// -----------------------------------------------------------------------------
juce::File PresetManager::getPresetsFolder() const
{
auto folder = juce::File::getSpecialLocation(
juce::File::userDocumentsDirectory)
.getChildFile(kSubFolder);
if (!folder.exists())
folder.createDirectory();
return folder;
}
juce::File PresetManager::getPresetFile(const juce::String& name) const
{
return getPresetsFolder().getChildFile(name + kExtension);
}
void PresetManager::refreshPresetList()
{
presetNames.clear();
auto files = getPresetsFolder().findChildFiles(
juce::File::findFiles, false,
juce::String("*") + kExtension);
files.sort();
for (const auto& f : files)
presetNames.add(f.getFileNameWithoutExtension());
}
// -----------------------------------------------------------------------------
// save
// -----------------------------------------------------------------------------
// reuses PluginProcessor::getStateInformation() directly,
// so no PluginProcessor changes are needed.
// -----------------------------------------------------------------------------
// --- after changes ---
bool PresetManager::savePreset(const juce::String& name)
{
if (name.isEmpty()) return false;
// * fix: notify the Processor of the name before getStateInformation()
// so getStateInformation() writes the correct name into the ValueTree
processor.setLastSavedPresetName(name);
juce::MemoryBlock data;
processor.getStateInformation(data);
auto file = getPresetFile(name);
if (!file.replaceWithData(data.getData(), data.getSize()))
return false;
currentPresetName = name;
refreshPresetList();
if (onPresetListChanged) onPresetListChanged();
if (onPresetLoaded) onPresetLoaded(name);
return true;
}
// -----------------------------------------------------------------------------
// load
// -----------------------------------------------------------------------------
// reuses PluginProcessor::setStateInformation() directly.
// setStateInformation() sets paramsNeedUpdate=true, so
// the next processBlock() sends the new parameters to the engine.
// -----------------------------------------------------------------------------
bool PresetManager::loadPreset(const juce::String& name)
{
auto file = getPresetFile(name);
if (!file.exists()) return false;
juce::MemoryBlock data;
if (!file.loadFileAsData(data)) return false;
processor.setStateInformation(data.getData(), static_cast<int>(data.getSize()));
// * after loading a preset always normal view (Normal Mode)
if (auto* param = processor.apvts.getParameter("promode"))
param->setValueNotifyingHost(0.0f);
currentPresetName = name;
if (onPresetLoaded) onPresetLoaded(name);
return true;
}
// -----------------------------------------------------------------------------
// delete
// -----------------------------------------------------------------------------
bool PresetManager::deletePreset(const juce::String& name)
{
auto file = getPresetFile(name);
if (!file.exists()) return false;
if (!file.deleteFile()) return false;
if (currentPresetName == name)
currentPresetName.clear();
refreshPresetList();
if (onPresetListChanged) onPresetListChanged();
return true;
}
// -----------------------------------------------------------------------------
// navigation
// -----------------------------------------------------------------------------
int PresetManager::getCurrentPresetIndex() const noexcept
{
return presetNames.indexOf(currentPresetName);
}
void PresetManager::loadPrevPreset()
{
if (presetNames.isEmpty()) return;
int idx = getCurrentPresetIndex();
if (idx <= 0)
idx = presetNames.size();
loadPreset(presetNames[idx - 1]);
}
void PresetManager::loadNextPreset()
{
if (presetNames.isEmpty()) return;
int idx = getCurrentPresetIndex();
if (idx < 0 || idx >= presetNames.size() - 1)
idx = -1;
loadPreset(presetNames[idx + 1]);
}

59
Source/PresetManager.h Normal file
View file

@ -0,0 +1,59 @@
#pragma once
#include <JuceHeader.h>
class FDNReverbAudioProcessor;
// -----------------------------------------------------------------------------
// PresetManager
// -----------------------------------------------------------------------------
// preset file manage .
//
// save location : ~/Documents/Ambivalence/Presets/*.ambpreset
// : APVTS getStateInformation/setStateInformation
// -> existing save fully reused , minimizes added code
//
// real-time safety :
// - file I/O all message thread (UI) assumed to be called
// - processBlock not involved at all
// -----------------------------------------------------------------------------
class PresetManager
{
public:
explicit PresetManager(FDNReverbAudioProcessor& processor);
// --- preset operations ---
bool savePreset(const juce::String& name);
bool loadPreset(const juce::String& name);
bool deletePreset(const juce::String& name);
// --- navigation ---
void loadPrevPreset();
void loadNextPreset();
// --- state access ---
juce::StringArray getPresetNames() const noexcept { return presetNames; }
// --- after changes ---
juce::String getCurrentPresetName() const noexcept { return currentPresetName; }
void setCurrentPresetName(const juce::String& name) noexcept { currentPresetName = name; } // * added
int getCurrentPresetIndex() const noexcept;
bool hasPresets() const noexcept { return !presetNames.isEmpty(); }
// --- folder access ---
juce::File getPresetsFolder() const;
// --- UI update callback ---
std::function<void()> onPresetListChanged;
std::function<void(const juce::String&)> onPresetLoaded;
private:
void refreshPresetList();
juce::File getPresetFile(const juce::String& name) const;
FDNReverbAudioProcessor& processor;
juce::StringArray presetNames;
juce::String currentPresetName;
static constexpr const char* kExtension = ".ambpreset";
static constexpr const char* kSubFolder = "Ambivalence/Presets";
};