commit b11135f78670645d6e70dbdf6f43e490a5e8d438 Author: hariel1985 Date: Wed Mar 25 10:17:59 2026 +0100 Initial commit: InstaLPEQ linear phase EQ plugin Full-featured linear phase EQ with interactive graphical curve display. FIR-based processing (8192-tap), 8 parametric bands, multi-platform CI/CD (Windows/macOS/Linux), InstaDrums visual style. Co-Authored-By: Claude Opus 4.6 (1M context) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..144c573 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,161 @@ +name: Build InstaLPEQ + +on: + push: + branches: [main] + tags: ['v*'] + pull_request: + branches: [main] + +jobs: + build-windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - name: Clone JUCE + run: git clone --depth 1 https://github.com/juce-framework/JUCE.git ../JUCE + + - name: Configure CMake + run: cmake -B build -G "Visual Studio 17 2022" -A x64 + + - name: Build Release + run: cmake --build build --config Release + + - name: Package VST3 + run: Compress-Archive -Path "build/InstaLPEQ_artefacts/Release/VST3/InstaLPEQ.vst3" -DestinationPath "InstaLPEQ-VST3-Win64.zip" + + - name: Package Standalone + run: Compress-Archive -Path "build/InstaLPEQ_artefacts/Release/Standalone/InstaLPEQ.exe" -DestinationPath "InstaLPEQ-Standalone-Win64.zip" + + - name: Upload VST3 + uses: actions/upload-artifact@v4 + with: + name: InstaLPEQ-VST3-Win64 + path: InstaLPEQ-VST3-Win64.zip + + - name: Upload Standalone + uses: actions/upload-artifact@v4 + with: + name: InstaLPEQ-Standalone-Win64 + path: InstaLPEQ-Standalone-Win64.zip + + build-macos: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - name: Clone JUCE + run: git clone --depth 1 https://github.com/juce-framework/JUCE.git ../JUCE + + - name: Configure CMake (Universal Binary) + run: cmake -B build -G Xcode -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 + + - name: Build Release + run: cmake --build build --config Release + + - name: Package VST3 + working-directory: build/InstaLPEQ_artefacts/Release + run: zip -r $GITHUB_WORKSPACE/InstaLPEQ-VST3-macOS.zip VST3/InstaLPEQ.vst3 + + - name: Package AU + working-directory: build/InstaLPEQ_artefacts/Release + run: zip -r $GITHUB_WORKSPACE/InstaLPEQ-AU-macOS.zip AU/InstaLPEQ.component + + - name: Package Standalone + working-directory: build/InstaLPEQ_artefacts/Release + run: zip -r $GITHUB_WORKSPACE/InstaLPEQ-Standalone-macOS.zip Standalone/InstaLPEQ.app + + - name: Upload VST3 + uses: actions/upload-artifact@v4 + with: + name: InstaLPEQ-VST3-macOS + path: InstaLPEQ-VST3-macOS.zip + + - name: Upload AU + uses: actions/upload-artifact@v4 + with: + name: InstaLPEQ-AU-macOS + path: InstaLPEQ-AU-macOS.zip + + - name: Upload Standalone + uses: actions/upload-artifact@v4 + with: + name: InstaLPEQ-Standalone-macOS + path: InstaLPEQ-Standalone-macOS.zip + + build-linux: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential cmake git libasound2-dev \ + libfreetype6-dev libx11-dev libxrandr-dev libxcursor-dev \ + libxinerama-dev libwebkit2gtk-4.1-dev libcurl4-openssl-dev + + - name: Clone JUCE + run: git clone --depth 1 https://github.com/juce-framework/JUCE.git ../JUCE + + - name: Configure CMake + run: cmake -B build -DCMAKE_BUILD_TYPE=Release + + - name: Build Release + run: cmake --build build --config Release --parallel $(nproc) + + - name: Package VST3 + working-directory: build/InstaLPEQ_artefacts/Release + run: zip -r $GITHUB_WORKSPACE/InstaLPEQ-VST3-Linux-x64.zip VST3/InstaLPEQ.vst3 + + - name: Package LV2 + working-directory: build/InstaLPEQ_artefacts/Release + run: zip -r $GITHUB_WORKSPACE/InstaLPEQ-LV2-Linux-x64.zip LV2/InstaLPEQ.lv2 + + - name: Package Standalone + run: zip -j InstaLPEQ-Standalone-Linux-x64.zip build/InstaLPEQ_artefacts/Release/Standalone/InstaLPEQ + + - name: Upload VST3 + uses: actions/upload-artifact@v4 + with: + name: InstaLPEQ-VST3-Linux-x64 + path: InstaLPEQ-VST3-Linux-x64.zip + + - name: Upload LV2 + uses: actions/upload-artifact@v4 + with: + name: InstaLPEQ-LV2-Linux-x64 + path: InstaLPEQ-LV2-Linux-x64.zip + + - name: Upload Standalone + uses: actions/upload-artifact@v4 + with: + name: InstaLPEQ-Standalone-Linux-x64 + path: InstaLPEQ-Standalone-Linux-x64.zip + + release: + if: startsWith(github.ref, 'refs/tags/v') + needs: [build-windows, build-macos, build-linux] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + files: | + artifacts/InstaLPEQ-VST3-Win64/InstaLPEQ-VST3-Win64.zip + artifacts/InstaLPEQ-Standalone-Win64/InstaLPEQ-Standalone-Win64.zip + artifacts/InstaLPEQ-VST3-macOS/InstaLPEQ-VST3-macOS.zip + artifacts/InstaLPEQ-AU-macOS/InstaLPEQ-AU-macOS.zip + artifacts/InstaLPEQ-Standalone-macOS/InstaLPEQ-Standalone-macOS.zip + artifacts/InstaLPEQ-VST3-Linux-x64/InstaLPEQ-VST3-Linux-x64.zip + artifacts/InstaLPEQ-LV2-Linux-x64/InstaLPEQ-LV2-Linux-x64.zip + artifacts/InstaLPEQ-Standalone-Linux-x64/InstaLPEQ-Standalone-Linux-x64.zip + generate_release_notes: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f0e2de1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +build/ +*.user +*.suo +.vs/ +CMakeSettings.json +out/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..7fc80f8 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,63 @@ +cmake_minimum_required(VERSION 3.22) +project(InstaLPEQ VERSION 1.0.0) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../JUCE ${CMAKE_CURRENT_BINARY_DIR}/JUCE) + +juce_add_plugin(InstaLPEQ + COMPANY_NAME "InstaLPEQ" + IS_SYNTH FALSE + NEEDS_MIDI_INPUT FALSE + NEEDS_MIDI_OUTPUT FALSE + PLUGIN_MANUFACTURER_CODE Inst + PLUGIN_CODE Ilpe + FORMATS VST3 AU LV2 Standalone + LV2URI "https://github.com/hariel1985/InstaLPEQ" + PRODUCT_NAME "InstaLPEQ" + COPY_PLUGIN_AFTER_BUILD FALSE +) + +juce_generate_juce_header(InstaLPEQ) + +juce_add_binary_data(InstaLPEQData SOURCES + Resources/Rajdhani-Regular.ttf + Resources/Rajdhani-Medium.ttf + Resources/Rajdhani-Bold.ttf +) + +target_sources(InstaLPEQ + PRIVATE + Source/PluginProcessor.cpp + Source/PluginEditor.cpp + Source/LookAndFeel.cpp + Source/EQCurveDisplay.cpp + Source/FIREngine.cpp + Source/NodeParameterPanel.cpp +) + +target_compile_definitions(InstaLPEQ + PUBLIC + JUCE_WEB_BROWSER=0 + JUCE_USE_CURL=0 + JUCE_VST3_CAN_REPLACE_VST2=0 +) + +target_link_libraries(InstaLPEQ + PRIVATE + InstaLPEQData + juce::juce_audio_basics + juce::juce_audio_devices + juce::juce_audio_formats + juce::juce_audio_processors + juce::juce_audio_utils + juce::juce_core + juce::juce_dsp + juce::juce_graphics + juce::juce_gui_basics + juce::juce_gui_extra + PUBLIC + juce::juce_recommended_config_flags + juce::juce_recommended_warning_flags +) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0492abc --- /dev/null +++ b/README.md @@ -0,0 +1,130 @@ +# InstaLPEQ + +Free, open-source linear phase EQ plugin built with JUCE. Available as VST3, AU, LV2 and Standalone. + +![VST3](https://img.shields.io/badge/format-VST3-blue) ![AU](https://img.shields.io/badge/format-AU-blue) ![LV2](https://img.shields.io/badge/format-LV2-blue) ![C++](https://img.shields.io/badge/language-C%2B%2B17-orange) ![JUCE](https://img.shields.io/badge/framework-JUCE-green) ![License](https://img.shields.io/badge/license-GPL--3.0-lightgrey) ![Build](https://github.com/hariel1985/InstaLPEQ/actions/workflows/build.yml/badge.svg) + +## Download + +**[Latest Release: v1.0](https://github.com/hariel1985/InstaLPEQ/releases/tag/v1.0)** + +### Windows +| File | Description | +|------|-------------| +| [InstaLPEQ-VST3-Win64.zip](https://github.com/hariel1985/InstaLPEQ/releases/download/v1.0/InstaLPEQ-VST3-Win64.zip) | VST3 plugin — copy to `C:\Program Files\Common Files\VST3\` | +| [InstaLPEQ-Standalone-Win64.zip](https://github.com/hariel1985/InstaLPEQ/releases/download/v1.0/InstaLPEQ-Standalone-Win64.zip) | Standalone application | + +### macOS (Universal Binary: Apple Silicon + Intel) +| File | Description | +|------|-------------| +| [InstaLPEQ-VST3-macOS.zip](https://github.com/hariel1985/InstaLPEQ/releases/download/v1.0/InstaLPEQ-VST3-macOS.zip) | VST3 plugin — copy to `~/Library/Audio/Plug-Ins/VST3/` | +| [InstaLPEQ-AU-macOS.zip](https://github.com/hariel1985/InstaLPEQ/releases/download/v1.0/InstaLPEQ-AU-macOS.zip) | Audio Unit — copy to `~/Library/Audio/Plug-Ins/Components/` | +| [InstaLPEQ-Standalone-macOS.zip](https://github.com/hariel1985/InstaLPEQ/releases/download/v1.0/InstaLPEQ-Standalone-macOS.zip) | Standalone application | + +### Linux (x64, built on Ubuntu 22.04) +| File | Description | +|------|-------------| +| [InstaLPEQ-VST3-Linux-x64.zip](https://github.com/hariel1985/InstaLPEQ/releases/download/v1.0/InstaLPEQ-VST3-Linux-x64.zip) | VST3 plugin — copy to `~/.vst3/` | +| [InstaLPEQ-LV2-Linux-x64.zip](https://github.com/hariel1985/InstaLPEQ/releases/download/v1.0/InstaLPEQ-LV2-Linux-x64.zip) | LV2 plugin — copy to `~/.lv2/` | +| [InstaLPEQ-Standalone-Linux-x64.zip](https://github.com/hariel1985/InstaLPEQ/releases/download/v1.0/InstaLPEQ-Standalone-Linux-x64.zip) | Standalone application | + +> **macOS note:** Builds are Universal Binary (Apple Silicon + Intel). Not code-signed — after copying the plugin, remove the quarantine flag in Terminal: +> ```bash +> xattr -cr ~/Library/Audio/Plug-Ins/VST3/InstaLPEQ.vst3 +> xattr -cr ~/Library/Audio/Plug-Ins/Components/InstaLPEQ.component +> ``` + +## Features + +### Linear Phase EQ +- True linear phase processing using symmetric FIR convolution +- Zero phase distortion at any gain setting +- 8192-tap FIR filter (configurable: 4096 / 8192 / 16384) +- DAW-compensated latency (~93ms at 44.1kHz default) +- Background thread FIR generation — glitch-free parameter changes + +### Interactive EQ Curve Display +- Logarithmic frequency axis (20 Hz — 20 kHz) +- Linear gain axis (-24 dB to +24 dB) +- Click to add EQ nodes (up to 8 bands) +- Drag nodes to adjust frequency and gain +- Scroll wheel to adjust Q/bandwidth +- Right-click for band type selection and delete +- Double-click to reset band to 0 dB +- Real-time frequency response curve with glow effect +- Per-band curve overlay + +### Band Types +- Peak (parametric) +- Low Shelf +- High Shelf + +### Controls +- Per-band: Frequency, Gain, Q knobs +- Master gain (+/- 24 dB) +- Bypass toggle +- State save/restore (DAW session recall) + +### GUI +- Dark modern UI matching InstaDrums visual style +- 3D metal knobs with glow effects (orange for EQ, blue for Q) +- Carbon fiber background texture +- Rajdhani custom font +- Fully resizable window with proportional scaling +- Animated toggle switches +- Color-coded EQ bands (8 distinct colors) + +## Building + +### Requirements +- CMake 3.22+ +- JUCE framework (cloned to `../JUCE` relative to project) + +#### Windows +- Visual Studio 2022 Build Tools (C++ workload) + +#### macOS +- Xcode 14+ + +#### Linux (Ubuntu 22.04+) +```bash +sudo apt-get install build-essential cmake git libasound2-dev \ + libfreetype6-dev libx11-dev libxrandr-dev libxcursor-dev \ + libxinerama-dev libwebkit2gtk-4.1-dev libcurl4-openssl-dev +``` + +### Build Steps + +```bash +git clone https://github.com/juce-framework/JUCE.git ../JUCE +cmake -B build -G "Visual Studio 17 2022" -A x64 # Windows +cmake -B build -G Xcode # macOS +cmake -B build -DCMAKE_BUILD_TYPE=Release # Linux +cmake --build build --config Release +``` + +Output: +- VST3: `build/InstaLPEQ_artefacts/Release/VST3/InstaLPEQ.vst3` +- AU: `build/InstaLPEQ_artefacts/Release/AU/InstaLPEQ.component` (macOS) +- LV2: `build/InstaLPEQ_artefacts/Release/LV2/InstaLPEQ.lv2` +- Standalone: `build/InstaLPEQ_artefacts/Release/Standalone/InstaLPEQ.exe` + +## How It Works + +InstaLPEQ uses a **FIR-based linear phase** approach: + +1. Each EQ band's target magnitude response is computed from IIR filter coefficients (Peak, Low Shelf, or High Shelf) +2. All band magnitudes are multiplied together to form the combined target response +3. An inverse FFT converts the magnitude-only spectrum into a symmetric time-domain impulse response +4. A Blackman-Harris window is applied to minimize truncation artifacts +5. The FIR filter is applied via JUCE's efficient FFT-based `Convolution` engine + +This ensures **zero phase distortion** regardless of EQ settings — ideal for mastering, surgical corrections, and transparent tonal shaping. + +## Tech Stack + +- **Language:** C++17 +- **Framework:** JUCE 8 +- **Build:** CMake + MSVC / Xcode / GCC +- **Audio DSP:** juce::dsp (FFT, Convolution, IIR coefficient design) +- **Font:** Rajdhani (SIL Open Font License) diff --git a/Resources/Rajdhani-Bold.ttf b/Resources/Rajdhani-Bold.ttf new file mode 100644 index 0000000..47af157 Binary files /dev/null and b/Resources/Rajdhani-Bold.ttf differ diff --git a/Resources/Rajdhani-Medium.ttf b/Resources/Rajdhani-Medium.ttf new file mode 100644 index 0000000..a6960c6 Binary files /dev/null and b/Resources/Rajdhani-Medium.ttf differ diff --git a/Resources/Rajdhani-Regular.ttf b/Resources/Rajdhani-Regular.ttf new file mode 100644 index 0000000..d25bd37 Binary files /dev/null and b/Resources/Rajdhani-Regular.ttf differ diff --git a/Source/EQBand.h b/Source/EQBand.h new file mode 100644 index 0000000..79da97c --- /dev/null +++ b/Source/EQBand.h @@ -0,0 +1,12 @@ +#pragma once + +struct EQBand +{ + enum Type { Peak, LowShelf, HighShelf }; + + float frequency = 1000.0f; // 20 Hz - 20000 Hz + float gainDb = 0.0f; // -24 dB to +24 dB + float q = 1.0f; // 0.1 to 18.0 + Type type = Peak; + bool enabled = true; +}; diff --git a/Source/EQCurveDisplay.cpp b/Source/EQCurveDisplay.cpp new file mode 100644 index 0000000..267db6c --- /dev/null +++ b/Source/EQCurveDisplay.cpp @@ -0,0 +1,420 @@ +#include "EQCurveDisplay.h" +#include "LookAndFeel.h" + +EQCurveDisplay::EQCurveDisplay() {} + +void EQCurveDisplay::setBands (const std::vector& newBands) +{ + bands = newBands; + repaint(); +} + +void EQCurveDisplay::setMagnitudeResponse (const std::vector& magnitudesDb, double sampleRate, int fftSize) +{ + magnitudeResponseDb = magnitudesDb; + responseSampleRate = sampleRate; + responseFftSize = fftSize; + repaint(); +} + +void EQCurveDisplay::setSelectedBand (int index) +{ + if (selectedBand != index) + { + selectedBand = index; + repaint(); + if (listener != nullptr) + listener->selectedBandChanged (index); + } +} + +// ============================================================ +// Coordinate mapping +// ============================================================ + +juce::Rectangle EQCurveDisplay::getPlotArea() const +{ + float marginL = 38.0f; + float marginR = 12.0f; + float marginT = 10.0f; + float marginB = 22.0f; + return getLocalBounds().toFloat().withTrimmedLeft (marginL) + .withTrimmedRight (marginR) + .withTrimmedTop (marginT) + .withTrimmedBottom (marginB); +} + +float EQCurveDisplay::freqToX (float freq) const +{ + auto area = getPlotArea(); + float normLog = std::log10 (freq / minFreq) / std::log10 (maxFreq / minFreq); + return area.getX() + normLog * area.getWidth(); +} + +float EQCurveDisplay::xToFreq (float x) const +{ + auto area = getPlotArea(); + float normLog = (x - area.getX()) / area.getWidth(); + return minFreq * std::pow (maxFreq / minFreq, normLog); +} + +float EQCurveDisplay::dbToY (float db) const +{ + auto area = getPlotArea(); + float norm = (maxDb - db) / (maxDb - minDb); + return area.getY() + norm * area.getHeight(); +} + +float EQCurveDisplay::yToDb (float y) const +{ + auto area = getPlotArea(); + float norm = (y - area.getY()) / area.getHeight(); + return maxDb - norm * (maxDb - minDb); +} + +// ============================================================ +// Paint +// ============================================================ + +void EQCurveDisplay::paint (juce::Graphics& g) +{ + auto bounds = getLocalBounds().toFloat(); + + // Background gradient + { + juce::ColourGradient bgGrad (InstaLPEQLookAndFeel::bgDark.darker (0.4f), 0, bounds.getY(), + InstaLPEQLookAndFeel::bgDark.darker (0.2f), 0, bounds.getBottom(), false); + g.setGradientFill (bgGrad); + g.fillRoundedRectangle (bounds, 4.0f); + } + + // Border + g.setColour (InstaLPEQLookAndFeel::bgLight.withAlpha (0.3f)); + g.drawRoundedRectangle (bounds, 4.0f, 1.0f); + + drawGrid (g); + drawPerBandCurves (g); + drawResponseCurve (g); + drawNodes (g); +} + +void EQCurveDisplay::drawGrid (juce::Graphics& g) +{ + auto area = getPlotArea(); + auto* lf = dynamic_cast (&getLookAndFeel()); + juce::Font labelFont = lf ? lf->getRegularFont (11.0f) : juce::Font (juce::FontOptions (11.0f)); + g.setFont (labelFont); + + // Vertical lines — frequency markers + const float freqs[] = { 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000 }; + const char* freqLabels[] = { "20", "50", "100", "200", "500", "1k", "2k", "5k", "10k", "20k" }; + + for (int i = 0; i < 10; ++i) + { + float xPos = freqToX (freqs[i]); + bool isMajor = (freqs[i] == 100 || freqs[i] == 1000 || freqs[i] == 10000); + g.setColour (InstaLPEQLookAndFeel::bgLight.withAlpha (isMajor ? 0.2f : 0.08f)); + g.drawVerticalLine ((int) xPos, area.getY(), area.getBottom()); + + g.setColour (InstaLPEQLookAndFeel::textSecondary.withAlpha (0.7f)); + g.drawText (freqLabels[i], (int) xPos - 16, (int) area.getBottom() + 2, 32, 16, + juce::Justification::centredTop, false); + } + + // Horizontal lines — dB markers + for (float db = minDb; db <= maxDb; db += 6.0f) + { + float yPos = dbToY (db); + bool isZero = (std::abs (db) < 0.1f); + g.setColour (isZero ? InstaLPEQLookAndFeel::accent.withAlpha (0.15f) + : InstaLPEQLookAndFeel::bgLight.withAlpha (0.1f)); + g.drawHorizontalLine ((int) yPos, area.getX(), area.getRight()); + + if (std::fmod (std::abs (db), 12.0f) < 0.1f || isZero) + { + g.setColour (InstaLPEQLookAndFeel::textSecondary.withAlpha (0.7f)); + juce::String label = (db > 0 ? "+" : "") + juce::String ((int) db); + g.drawText (label, (int) area.getX() - 36, (int) yPos - 8, 32, 16, + juce::Justification::centredRight, false); + } + } +} + +void EQCurveDisplay::drawResponseCurve (juce::Graphics& g) +{ + if (magnitudeResponseDb.empty()) + return; + + auto area = getPlotArea(); + int numBins = (int) magnitudeResponseDb.size(); + + juce::Path curvePath; + juce::Path fillPath; + float zeroY = dbToY (0.0f); + bool started = false; + + for (float px = area.getX(); px <= area.getRight(); px += 1.0f) + { + float freq = xToFreq (px); + if (freq < 1.0f || freq > responseSampleRate * 0.5) + continue; + + // Convert frequency to bin index + float binFloat = freq * (float) responseFftSize / (float) responseSampleRate; + int bin = (int) binFloat; + float frac = binFloat - (float) bin; + + if (bin < 0 || bin >= numBins - 1) + continue; + + // Linear interpolation between bins + float dbVal = magnitudeResponseDb[bin] * (1.0f - frac) + magnitudeResponseDb[bin + 1] * frac; + dbVal = juce::jlimit (minDb - 6.0f, maxDb + 6.0f, dbVal); + float yPos = dbToY (dbVal); + + if (! started) + { + curvePath.startNewSubPath (px, yPos); + fillPath.startNewSubPath (px, zeroY); + fillPath.lineTo (px, yPos); + started = true; + } + else + { + curvePath.lineTo (px, yPos); + fillPath.lineTo (px, yPos); + } + } + + if (! started) + return; + + // Close fill path + fillPath.lineTo (area.getRight(), zeroY); + fillPath.closeSubPath(); + + // Fill under curve + g.setColour (InstaLPEQLookAndFeel::accent.withAlpha (0.1f)); + g.fillPath (fillPath); + + // Glow + g.setColour (InstaLPEQLookAndFeel::accent.withAlpha (0.2f)); + g.strokePath (curvePath, juce::PathStrokeType (4.0f)); + + // Core curve + g.setColour (InstaLPEQLookAndFeel::accent); + g.strokePath (curvePath, juce::PathStrokeType (2.0f)); +} + +void EQCurveDisplay::drawPerBandCurves (juce::Graphics& g) +{ + if (bands.empty()) + return; + + auto area = getPlotArea(); + + for (int bandIdx = 0; bandIdx < (int) bands.size(); ++bandIdx) + { + const auto& band = bands[bandIdx]; + if (! band.enabled || std::abs (band.gainDb) < 0.01f) + continue; + + float gainLinear = juce::Decibels::decibelsToGain (band.gainDb); + juce::dsp::IIR::Coefficients::Ptr coeffs; + + switch (band.type) + { + case EQBand::Peak: + coeffs = juce::dsp::IIR::Coefficients::makePeakFilter (responseSampleRate, band.frequency, band.q, gainLinear); + break; + case EQBand::LowShelf: + coeffs = juce::dsp::IIR::Coefficients::makeLowShelf (responseSampleRate, band.frequency, band.q, gainLinear); + break; + case EQBand::HighShelf: + coeffs = juce::dsp::IIR::Coefficients::makeHighShelf (responseSampleRate, band.frequency, band.q, gainLinear); + break; + } + + if (coeffs == nullptr) + continue; + + juce::Path bandPath; + bool started = false; + auto colour = nodeColours[bandIdx % 8].withAlpha (bandIdx == selectedBand ? 0.4f : 0.15f); + + for (float px = area.getX(); px <= area.getRight(); px += 2.0f) + { + float freq = xToFreq (px); + if (freq < 1.0f) + continue; + + double mag = coeffs->getMagnitudeForFrequency (freq, responseSampleRate); + float dbVal = (float) juce::Decibels::gainToDecibels (mag, -60.0); + dbVal = juce::jlimit (minDb - 6.0f, maxDb + 6.0f, dbVal); + float yPos = dbToY (dbVal); + + if (! started) { bandPath.startNewSubPath (px, yPos); started = true; } + else bandPath.lineTo (px, yPos); + } + + g.setColour (colour); + g.strokePath (bandPath, juce::PathStrokeType (1.5f)); + } +} + +void EQCurveDisplay::drawNodes (juce::Graphics& g) +{ + for (int i = 0; i < (int) bands.size(); ++i) + { + const auto& band = bands[i]; + float nx = freqToX (band.frequency); + float ny = dbToY (band.gainDb); + auto colour = nodeColours[i % 8]; + bool isSel = (i == selectedBand); + + float r = isSel ? 10.0f : 8.0f; + + // Glow for selected + if (isSel) + { + for (int gl = 0; gl < 3; ++gl) + { + float t = (float) gl / 2.0f; + float gr = r * (2.0f - t * 0.6f); + float alpha = 0.05f + t * t * 0.15f; + g.setColour (colour.withAlpha (alpha)); + g.fillEllipse (nx - gr, ny - gr, gr * 2, gr * 2); + } + } + + // Fill + g.setColour (band.enabled ? colour : colour.withAlpha (0.4f)); + g.fillEllipse (nx - r, ny - r, r * 2, r * 2); + + // Border + g.setColour (isSel ? juce::Colours::white : colour.brighter (0.3f)); + g.drawEllipse (nx - r, ny - r, r * 2, r * 2, isSel ? 2.0f : 1.0f); + + // Band number + auto* lf = dynamic_cast (&getLookAndFeel()); + juce::Font numFont = lf ? lf->getBoldFont (11.0f) : juce::Font (juce::FontOptions (11.0f)); + g.setFont (numFont); + g.setColour (juce::Colours::white); + g.drawText (juce::String (i + 1), (int) (nx - r), (int) (ny - r), (int) (r * 2), (int) (r * 2), + juce::Justification::centred, false); + } +} + +// ============================================================ +// Mouse interaction +// ============================================================ + +int EQCurveDisplay::findNodeAt (float x, float y, float radius) const +{ + for (int i = 0; i < (int) bands.size(); ++i) + { + float nx = freqToX (bands[i].frequency); + float ny = dbToY (bands[i].gainDb); + float dist = std::sqrt ((x - nx) * (x - nx) + (y - ny) * (y - ny)); + if (dist <= radius) + return i; + } + return -1; +} + +void EQCurveDisplay::mouseDown (const juce::MouseEvent& e) +{ + auto pos = e.position; + int hit = findNodeAt (pos.x, pos.y); + + if (e.mods.isRightButtonDown() && hit >= 0) + { + // Right-click context menu + juce::PopupMenu menu; + menu.addItem (1, "Delete Band"); + menu.addItem (2, "Reset to 0 dB"); + menu.addSeparator(); + menu.addItem (3, "Peak", true, bands[hit].type == EQBand::Peak); + menu.addItem (4, "Low Shelf", true, bands[hit].type == EQBand::LowShelf); + menu.addItem (5, "High Shelf", true, bands[hit].type == EQBand::HighShelf); + + menu.showMenuAsync (juce::PopupMenu::Options(), [this, hit] (int result) + { + if (result == 1) + { + if (listener) listener->bandRemoved (hit); + } + else if (result == 2) + { + auto band = bands[hit]; + band.gainDb = 0.0f; + if (listener) listener->bandChanged (hit, band); + } + else if (result >= 3 && result <= 5) + { + auto band = bands[hit]; + band.type = (result == 3) ? EQBand::Peak : (result == 4) ? EQBand::LowShelf : EQBand::HighShelf; + if (listener) listener->bandChanged (hit, band); + } + }); + return; + } + + if (hit >= 0) + { + draggedBand = hit; + setSelectedBand (hit); + } + else if (e.mods.isLeftButtonDown() && (int) bands.size() < 8) + { + // Add new band + float freq = juce::jlimit (minFreq, maxFreq, xToFreq (pos.x)); + float gain = juce::jlimit (minDb, maxDb, yToDb (pos.y)); + if (listener) + listener->bandAdded ((int) bands.size(), freq, gain); + } +} + +void EQCurveDisplay::mouseDrag (const juce::MouseEvent& e) +{ + if (draggedBand < 0 || draggedBand >= (int) bands.size()) + return; + + auto pos = e.position; + auto band = bands[draggedBand]; + band.frequency = juce::jlimit (minFreq, maxFreq, xToFreq (pos.x)); + band.gainDb = juce::jlimit (minDb, maxDb, yToDb (pos.y)); + + if (listener) + listener->bandChanged (draggedBand, band); +} + +void EQCurveDisplay::mouseUp (const juce::MouseEvent&) +{ + draggedBand = -1; +} + +void EQCurveDisplay::mouseDoubleClick (const juce::MouseEvent& e) +{ + int hit = findNodeAt (e.position.x, e.position.y); + if (hit >= 0) + { + auto band = bands[hit]; + band.gainDb = 0.0f; + if (listener) + listener->bandChanged (hit, band); + } +} + +void EQCurveDisplay::mouseWheelMove (const juce::MouseEvent& e, const juce::MouseWheelDetails& wheel) +{ + int hit = findNodeAt (e.position.x, e.position.y, 20.0f); + if (hit >= 0) + { + auto band = bands[hit]; + float delta = wheel.deltaY * 2.0f; + band.q = juce::jlimit (0.1f, 18.0f, band.q + delta); + if (listener) + listener->bandChanged (hit, band); + } +} diff --git a/Source/EQCurveDisplay.h b/Source/EQCurveDisplay.h new file mode 100644 index 0000000..e1f49f1 --- /dev/null +++ b/Source/EQCurveDisplay.h @@ -0,0 +1,71 @@ +#pragma once +#include +#include "EQBand.h" + +class EQCurveDisplay : public juce::Component +{ +public: + struct Listener + { + virtual ~Listener() = default; + virtual void bandAdded (int index, float freq, float gainDb) = 0; + virtual void bandRemoved (int index) = 0; + virtual void bandChanged (int index, const EQBand& band) = 0; + virtual void selectedBandChanged (int index) = 0; + }; + + EQCurveDisplay(); + + void setListener (Listener* l) { listener = l; } + void setBands (const std::vector& bands); + void setMagnitudeResponse (const std::vector& magnitudesDb, double sampleRate, int fftSize); + int getSelectedBandIndex() const { return selectedBand; } + void setSelectedBand (int index); + + void paint (juce::Graphics& g) override; + void mouseDown (const juce::MouseEvent& e) override; + void mouseDrag (const juce::MouseEvent& e) override; + void mouseUp (const juce::MouseEvent& e) override; + void mouseDoubleClick (const juce::MouseEvent& e) override; + void mouseWheelMove (const juce::MouseEvent& e, const juce::MouseWheelDetails& w) override; + +private: + std::vector bands; + std::vector magnitudeResponseDb; + double responseSampleRate = 44100.0; + int responseFftSize = 8192; + int selectedBand = -1; + int draggedBand = -1; + Listener* listener = nullptr; + + static constexpr float minFreq = 20.0f; + static constexpr float maxFreq = 20000.0f; + static constexpr float minDb = -24.0f; + static constexpr float maxDb = 24.0f; + + // Node colours (8 distinct colours for up to 8 bands) + static inline const juce::Colour nodeColours[8] = { + juce::Colour (0xffff6644), // orange-red + juce::Colour (0xff44bbff), // sky blue + juce::Colour (0xffff44aa), // pink + juce::Colour (0xff44ff88), // green + juce::Colour (0xffffff44), // yellow + juce::Colour (0xffaa44ff), // purple + juce::Colour (0xff44ffff), // cyan + juce::Colour (0xffff8844), // orange + }; + + juce::Rectangle getPlotArea() const; + float freqToX (float freq) const; + float xToFreq (float x) const; + float dbToY (float db) const; + float yToDb (float y) const; + + void drawGrid (juce::Graphics& g); + void drawResponseCurve (juce::Graphics& g); + void drawPerBandCurves (juce::Graphics& g); + void drawNodes (juce::Graphics& g); + int findNodeAt (float x, float y, float radius = 14.0f) const; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (EQCurveDisplay) +}; diff --git a/Source/FIREngine.cpp b/Source/FIREngine.cpp new file mode 100644 index 0000000..4c68686 --- /dev/null +++ b/Source/FIREngine.cpp @@ -0,0 +1,172 @@ +#include "FIREngine.h" + +FIREngine::FIREngine() : Thread ("FIREngine") {} + +FIREngine::~FIREngine() +{ + stop(); +} + +void FIREngine::start (double sr) +{ + sampleRate.store (sr); + needsUpdate.store (true); + startThread (juce::Thread::Priority::normal); +} + +void FIREngine::stop() +{ + signalThreadShouldExit(); + notify(); + stopThread (2000); +} + +void FIREngine::setBands (const std::vector& newBands) +{ + { + const juce::SpinLock::ScopedLockType lock (bandLock); + currentBands = newBands; + } + needsUpdate.store (true); + notify(); +} + +void FIREngine::setFFTOrder (int order) +{ + fftOrder.store (juce::jlimit (12, 14, order)); + needsUpdate.store (true); + notify(); +} + +std::unique_ptr> FIREngine::getNewFIR() +{ + const juce::SpinLock::ScopedTryLockType lock (firLock); + if (lock.isLocked() && pendingFIR != nullptr) + return std::move (pendingFIR); + return nullptr; +} + +std::vector FIREngine::getMagnitudeResponseDb() const +{ + const juce::SpinLock::ScopedLockType lock (magLock); + return magnitudeDb; +} + +void FIREngine::run() +{ + while (! threadShouldExit()) + { + if (needsUpdate.exchange (false)) + { + std::vector bands; + { + const juce::SpinLock::ScopedLockType lock (bandLock); + bands = currentBands; + } + + auto fir = generateFIR (bands, sampleRate.load(), fftOrder.load()); + + { + const juce::SpinLock::ScopedLockType lock (firLock); + pendingFIR = std::make_unique> (std::move (fir)); + } + } + + wait (50); // Check every 50ms + } +} + +juce::AudioBuffer FIREngine::generateFIR (const std::vector& bands, double sr, int order) +{ + const int fftSize = 1 << order; + const int numBins = fftSize / 2 + 1; + + // Compute frequency for each FFT bin + std::vector frequencies (numBins); + for (int i = 0; i < numBins; ++i) + frequencies[i] = (double) i * sr / (double) fftSize; + + // Start with flat magnitude response (1.0 = 0dB) + std::vector magnitudes (numBins, 1.0); + + // For each active band, compute its magnitude contribution and multiply in + for (const auto& band : bands) + { + if (! band.enabled || std::abs (band.gainDb) < 0.01f) + continue; + + float gainLinear = juce::Decibels::decibelsToGain (band.gainDb); + + // Create IIR coefficients just for magnitude response analysis + juce::dsp::IIR::Coefficients::Ptr coeffs; + + switch (band.type) + { + case EQBand::Peak: + coeffs = juce::dsp::IIR::Coefficients::makePeakFilter (sr, band.frequency, band.q, gainLinear); + break; + case EQBand::LowShelf: + coeffs = juce::dsp::IIR::Coefficients::makeLowShelf (sr, band.frequency, band.q, gainLinear); + break; + case EQBand::HighShelf: + coeffs = juce::dsp::IIR::Coefficients::makeHighShelf (sr, band.frequency, band.q, gainLinear); + break; + } + + if (coeffs == nullptr) + continue; + + // Get magnitude for each bin + std::vector bandMag (numBins); + coeffs->getMagnitudeForFrequencyArray (frequencies.data(), bandMag.data(), numBins, sr); + + for (int i = 0; i < numBins; ++i) + magnitudes[i] *= bandMag[i]; + } + + // Store magnitude in dB for display + { + std::vector magDb (numBins); + for (int i = 0; i < numBins; ++i) + magDb[i] = (float) juce::Decibels::gainToDecibels (magnitudes[i], -60.0); + + const juce::SpinLock::ScopedLockType lock (magLock); + magnitudeDb = std::move (magDb); + } + + // Build complex spectrum: magnitude only, zero phase (linear phase) + // JUCE FFT expects interleaved [real, imag, real, imag, ...] for complex + // For performRealOnlyInverseTransform, input is fftSize*2 floats + std::vector fftData (fftSize * 2, 0.0f); + + // Pack magnitude into real parts (positive frequencies) + // performRealOnlyInverseTransform expects the format from performRealOnlyForwardTransform: + // data[0] = DC real, data[1] = Nyquist real, then interleaved complex pairs + fftData[0] = (float) magnitudes[0]; // DC + fftData[1] = (float) magnitudes[numBins - 1]; // Nyquist + + for (int i = 1; i < numBins - 1; ++i) + { + fftData[i * 2] = (float) magnitudes[i]; // real + fftData[i * 2 + 1] = 0.0f; // imag (zero = linear phase) + } + + // Inverse FFT to get time-domain impulse response + juce::dsp::FFT fft (order); + fft.performRealOnlyInverseTransform (fftData.data()); + + // The result is in fftData[0..fftSize-1] + // Circular shift by fftSize/2 to center the impulse (make it causal) + juce::AudioBuffer firBuffer (1, fftSize); + float* firData = firBuffer.getWritePointer (0); + int halfSize = fftSize / 2; + + for (int i = 0; i < fftSize; ++i) + firData[i] = fftData[(i + halfSize) % fftSize]; + + // Apply window to reduce truncation artifacts + juce::dsp::WindowingFunction window (fftSize, juce::dsp::WindowingFunction::blackmanHarris); + window.multiplyWithWindowingTable (firData, fftSize); + + return firBuffer; +} diff --git a/Source/FIREngine.h b/Source/FIREngine.h new file mode 100644 index 0000000..6e6902c --- /dev/null +++ b/Source/FIREngine.h @@ -0,0 +1,46 @@ +#pragma once +#include +#include "EQBand.h" + +class FIREngine : private juce::Thread +{ +public: + static constexpr int defaultFFTOrder = 13; // 8192 taps + static constexpr int maxBands = 8; + + FIREngine(); + ~FIREngine() override; + + void start (double sampleRate); + void stop(); + + // Called from GUI thread + void setBands (const std::vector& newBands); + void setFFTOrder (int order); + + // Called from audio thread — returns new FIR if available, nullptr otherwise + std::unique_ptr> getNewFIR(); + + // Get magnitude response in dB for display (thread-safe copy) + std::vector getMagnitudeResponseDb() const; + + int getFIRLength() const { return 1 << fftOrder.load(); } + int getLatencySamples() const { return getFIRLength() / 2; } + +private: + void run() override; + juce::AudioBuffer generateFIR (const std::vector& bands, double sr, int order); + + std::atomic sampleRate { 44100.0 }; + std::atomic fftOrder { defaultFFTOrder }; + std::atomic needsUpdate { true }; + + std::vector currentBands; + mutable juce::SpinLock bandLock; + + std::unique_ptr> pendingFIR; + juce::SpinLock firLock; + + std::vector magnitudeDb; + mutable juce::SpinLock magLock; +}; diff --git a/Source/LookAndFeel.cpp b/Source/LookAndFeel.cpp new file mode 100644 index 0000000..24bfd4a --- /dev/null +++ b/Source/LookAndFeel.cpp @@ -0,0 +1,337 @@ +#include "LookAndFeel.h" +#include "BinaryData.h" + +InstaLPEQLookAndFeel::InstaLPEQLookAndFeel() +{ + typefaceRegular = juce::Typeface::createSystemTypefaceFor ( + BinaryData::RajdhaniRegular_ttf, BinaryData::RajdhaniRegular_ttfSize); + typefaceMedium = juce::Typeface::createSystemTypefaceFor ( + BinaryData::RajdhaniMedium_ttf, BinaryData::RajdhaniMedium_ttfSize); + typefaceBold = juce::Typeface::createSystemTypefaceFor ( + BinaryData::RajdhaniBold_ttf, BinaryData::RajdhaniBold_ttfSize); + + setColour (juce::ResizableWindow::backgroundColourId, bgDark); + setColour (juce::Label::textColourId, textPrimary); + setColour (juce::TextButton::buttonColourId, bgMedium); + setColour (juce::TextButton::textColourOffId, textPrimary); + + generateNoiseTexture(); +} + +juce::Typeface::Ptr InstaLPEQLookAndFeel::getTypefaceForFont (const juce::Font& font) +{ + if (font.isBold()) + return typefaceBold; + return typefaceRegular; +} + +juce::Font InstaLPEQLookAndFeel::getRegularFont (float height) const +{ + return juce::Font (juce::FontOptions (typefaceRegular).withHeight (height)); +} + +juce::Font InstaLPEQLookAndFeel::getMediumFont (float height) const +{ + return juce::Font (juce::FontOptions (typefaceMedium).withHeight (height)); +} + +juce::Font InstaLPEQLookAndFeel::getBoldFont (float height) const +{ + return juce::Font (juce::FontOptions (typefaceBold).withHeight (height)); +} + +void InstaLPEQLookAndFeel::generateNoiseTexture() +{ + const int texW = 256, texH = 256; + noiseTexture = juce::Image (juce::Image::ARGB, texW, texH, true); + + juce::Random rng (42); + + for (int y = 0; y < texH; ++y) + { + for (int x = 0; x < texW; ++x) + { + float noise = rng.nextFloat() * 0.06f; + bool crossA = ((x + y) % 4 == 0); + bool crossB = ((x - y + 256) % 4 == 0); + float pattern = (crossA || crossB) ? 0.03f : 0.0f; + float alpha = noise + pattern; + noiseTexture.setPixelAt (x, y, juce::Colour::fromFloatRGBA (1.0f, 1.0f, 1.0f, alpha)); + } + } +} + +void InstaLPEQLookAndFeel::drawBackgroundTexture (juce::Graphics& g, juce::Rectangle area) +{ + for (int y = area.getY(); y < area.getBottom(); y += noiseTexture.getHeight()) + for (int x = area.getX(); x < area.getRight(); x += noiseTexture.getWidth()) + g.drawImageAt (noiseTexture, x, y); +} + +// ============================================================ +// Rotary slider (3D metal knob) +// ============================================================ + +void InstaLPEQLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, int width, int height, + float sliderPos, float rotaryStartAngle, + float rotaryEndAngle, juce::Slider& slider) +{ + float knobSize = std::min ((float) width, (float) height); + float s = knobSize / 60.0f; + float margin = std::max (4.0f, 6.0f * s); + + auto bounds = juce::Rectangle (x, y, width, height).toFloat().reduced (margin); + auto radius = std::min (bounds.getWidth(), bounds.getHeight()) / 2.0f; + auto cx = bounds.getCentreX(); + auto cy = bounds.getCentreY(); + auto angle = rotaryStartAngle + sliderPos * (rotaryEndAngle - rotaryStartAngle); + + auto knobType = slider.getProperties() [knobTypeProperty].toString(); + bool isDark = (knobType == "dark"); + + juce::Colour arcColour = isDark ? juce::Colour (0xff4488ff) : juce::Colour (0xffff8833); + juce::Colour arcBgColour = isDark ? juce::Colour (0xff1a2a44) : juce::Colour (0xff2a1a0a); + juce::Colour bodyTop = isDark ? juce::Colour (0xff3a3a4a) : juce::Colour (0xff5a4a3a); + juce::Colour bodyBottom = isDark ? juce::Colour (0xff1a1a2a) : juce::Colour (0xff2a1a0a); + juce::Colour rimColour = isDark ? juce::Colour (0xff555566) : juce::Colour (0xff886644); + juce::Colour highlightCol = isDark ? juce::Colour (0x33aabbff) : juce::Colour (0x44ffcc88); + juce::Colour pointerColour = isDark ? juce::Colour (0xff66aaff) : juce::Colour (0xffffaa44); + + float arcW = std::max (1.5f, 2.5f * s); + float glowW1 = std::max (3.0f, 10.0f * s); + float hotW = std::max (0.8f, 1.2f * s); + float ptrW = std::max (1.2f, 2.0f * s); + float bodyRadius = radius * 0.72f; + + // 1. Drop shadow + g.setColour (juce::Colours::black.withAlpha (0.35f)); + g.fillEllipse (cx - bodyRadius + 1, cy - bodyRadius + 2, bodyRadius * 2, bodyRadius * 2); + + // 2. Outer arc track + { + juce::Path arcBg; + arcBg.addCentredArc (cx, cy, radius - 1, radius - 1, 0.0f, + rotaryStartAngle, rotaryEndAngle, true); + g.setColour (arcBgColour); + g.strokePath (arcBg, juce::PathStrokeType (arcW, juce::PathStrokeType::curved, + juce::PathStrokeType::rounded)); + } + + // 3. Outer arc value with smooth multi-layer glow + if (sliderPos > 0.01f) + { + juce::Path arcVal; + arcVal.addCentredArc (cx, cy, radius - 1, radius - 1, 0.0f, + rotaryStartAngle, angle, true); + + const int numGlowLayers = 8; + for (int i = 0; i < numGlowLayers; ++i) + { + float t = (float) i / (float) (numGlowLayers - 1); + float layerWidth = glowW1 * (1.0f - t * 0.7f); + float layerAlpha = 0.03f + t * t * 0.35f; + g.setColour (arcColour.withAlpha (layerAlpha)); + g.strokePath (arcVal, juce::PathStrokeType (layerWidth, juce::PathStrokeType::curved, + juce::PathStrokeType::rounded)); + } + + g.setColour (arcColour); + g.strokePath (arcVal, juce::PathStrokeType (arcW, juce::PathStrokeType::curved, + juce::PathStrokeType::rounded)); + + g.setColour (arcColour.brighter (0.6f).withAlpha (0.5f)); + g.strokePath (arcVal, juce::PathStrokeType (hotW, juce::PathStrokeType::curved, + juce::PathStrokeType::rounded)); + } + + // 4. Knob body + { + juce::ColourGradient bodyGrad (bodyTop, cx, cy - bodyRadius * 0.5f, + bodyBottom, cx, cy + bodyRadius, true); + g.setGradientFill (bodyGrad); + g.fillEllipse (cx - bodyRadius, cy - bodyRadius, bodyRadius * 2, bodyRadius * 2); + } + + // 5. Rim + g.setColour (rimColour.withAlpha (0.6f)); + g.drawEllipse (cx - bodyRadius, cy - bodyRadius, bodyRadius * 2, bodyRadius * 2, std::max (0.8f, 1.2f * s)); + + // 6. Inner shadow + { + float innerR = bodyRadius * 0.85f; + juce::ColourGradient innerGrad (juce::Colours::black.withAlpha (0.15f), cx, cy - innerR * 0.3f, + juce::Colours::transparentBlack, cx, cy + innerR, true); + g.setGradientFill (innerGrad); + g.fillEllipse (cx - innerR, cy - innerR, innerR * 2, innerR * 2); + } + + // 7. Top highlight + { + float hlRadius = bodyRadius * 0.55f; + float hlY = cy - bodyRadius * 0.35f; + juce::ColourGradient hlGrad (highlightCol, cx, hlY - hlRadius * 0.5f, + juce::Colours::transparentBlack, cx, hlY + hlRadius, true); + g.setGradientFill (hlGrad); + g.fillEllipse (cx - hlRadius, hlY - hlRadius * 0.6f, hlRadius * 2, hlRadius * 1.2f); + } + + // 8. Pointer with subtle glow + { + float pointerLen = bodyRadius * 0.75f; + + for (int i = 0; i < 4; ++i) + { + float t = (float) i / 3.0f; + float gw = ptrW * (2.0f - t * 1.5f); + float alpha = 0.02f + t * t * 0.15f; + + juce::Path glowLayer; + glowLayer.addRoundedRectangle (-gw, -pointerLen, gw * 2, pointerLen * 0.55f, gw * 0.5f); + glowLayer.applyTransform (juce::AffineTransform::rotation (angle).translated (cx, cy)); + g.setColour (pointerColour.withAlpha (alpha)); + g.fillPath (glowLayer); + } + + { + juce::Path pointer; + pointer.addRoundedRectangle (-ptrW * 0.5f, -pointerLen, ptrW, pointerLen * 0.55f, ptrW * 0.5f); + pointer.applyTransform (juce::AffineTransform::rotation (angle).translated (cx, cy)); + g.setColour (pointerColour); + g.fillPath (pointer); + } + + { + juce::Path hotCenter; + float hw = ptrW * 0.3f; + hotCenter.addRoundedRectangle (-hw, -pointerLen, hw * 2, pointerLen * 0.5f, hw); + hotCenter.applyTransform (juce::AffineTransform::rotation (angle).translated (cx, cy)); + g.setColour (pointerColour.brighter (0.7f).withAlpha (0.6f)); + g.fillPath (hotCenter); + } + } + + // 9. Center cap + { + float capR = bodyRadius * 0.18f; + juce::ColourGradient capGrad (rimColour.brighter (0.3f), cx, cy - capR, + bodyBottom, cx, cy + capR, false); + g.setGradientFill (capGrad); + g.fillEllipse (cx - capR, cy - capR, capR * 2, capR * 2); + } +} + +// ============================================================ +// Button style +// ============================================================ + +void InstaLPEQLookAndFeel::drawButtonBackground (juce::Graphics& g, juce::Button& button, + const juce::Colour& backgroundColour, + bool shouldDrawButtonAsHighlighted, + bool shouldDrawButtonAsDown) +{ + auto bounds = button.getLocalBounds().toFloat().reduced (0.5f); + + auto baseColour = backgroundColour; + if (shouldDrawButtonAsDown) + baseColour = baseColour.brighter (0.2f); + else if (shouldDrawButtonAsHighlighted) + baseColour = baseColour.brighter (0.1f); + + juce::ColourGradient grad (baseColour.brighter (0.05f), 0, bounds.getY(), + baseColour.darker (0.1f), 0, bounds.getBottom(), false); + g.setGradientFill (grad); + g.fillRoundedRectangle (bounds, 4.0f); + + g.setColour (bgLight.withAlpha (shouldDrawButtonAsHighlighted ? 0.8f : 0.5f)); + g.drawRoundedRectangle (bounds, 4.0f, 1.0f); +} + +// ============================================================ +// Toggle button — glowing switch +// ============================================================ + +void InstaLPEQLookAndFeel::drawToggleButton (juce::Graphics& g, juce::ToggleButton& button, + bool shouldDrawButtonAsHighlighted, + bool /*shouldDrawButtonAsDown*/) +{ + auto bounds = button.getLocalBounds().toFloat(); + float h = std::min (bounds.getHeight() * 0.6f, 14.0f); + float w = h * 1.8f; + float trackR = h * 0.5f; + + float sx = bounds.getX() + (bounds.getWidth() - w) * 0.5f; + float sy = bounds.getCentreY() - h * 0.5f; + auto trackBounds = juce::Rectangle (sx, sy, w, h); + + bool isOn = button.getToggleState(); + auto onColour = accent; + auto offColour = bgLight; + + float targetPos = isOn ? 1.0f : 0.0f; + float animPos = (float) button.getProperties().getWithDefault ("animPos", targetPos); + animPos += (targetPos - animPos) * 0.25f; + if (std::abs (animPos - targetPos) < 0.01f) animPos = targetPos; + button.getProperties().set ("animPos", animPos); + + if (std::abs (animPos - targetPos) > 0.005f) + button.repaint(); + + float thumbR = h * 0.4f; + float thumbX = sx + trackR + animPos * (w - trackR * 2); + float thumbY = sy + h * 0.5f; + float glowIntensity = animPos; + + if (glowIntensity > 0.01f) + { + for (int i = 0; i < 3; ++i) + { + float t = (float) i / 2.0f; + float expand = (1.0f - t) * 1.5f; + float alpha = (0.04f + t * t * 0.1f) * glowIntensity; + g.setColour (onColour.withAlpha (alpha)); + g.fillRoundedRectangle (trackBounds.expanded (expand), trackR + expand); + } + } + + { + juce::Colour offCol = offColour.withAlpha (0.3f); + juce::Colour onCol = onColour.withAlpha (0.35f); + juce::Colour trackCol = offCol.interpolatedWith (onCol, glowIntensity); + if (shouldDrawButtonAsHighlighted) + trackCol = trackCol.brighter (0.15f); + g.setColour (trackCol); + g.fillRoundedRectangle (trackBounds, trackR); + + g.setColour (offColour.withAlpha (0.4f).interpolatedWith (onColour.withAlpha (0.5f), glowIntensity)); + g.drawRoundedRectangle (trackBounds, trackR, 0.8f); + } + + if (glowIntensity > 0.01f) + { + for (int i = 0; i < 3; ++i) + { + float t = (float) i / 2.0f; + float r = thumbR * (1.5f - t * 0.5f); + float alpha = (0.05f + t * t * 0.12f) * glowIntensity; + g.setColour (onColour.withAlpha (alpha)); + g.fillEllipse (thumbX - r, thumbY - r, r * 2, r * 2); + } + } + + { + juce::Colour thumbTopOff (0xff555566), thumbBotOff (0xff333344); + juce::Colour thumbTopOn = onColour.brighter (0.3f), thumbBotOn = onColour.darker (0.2f); + juce::ColourGradient thumbGrad ( + thumbTopOff.interpolatedWith (thumbTopOn, glowIntensity), thumbX, thumbY - thumbR, + thumbBotOff.interpolatedWith (thumbBotOn, glowIntensity), thumbX, thumbY + thumbR, false); + g.setGradientFill (thumbGrad); + g.fillEllipse (thumbX - thumbR, thumbY - thumbR, thumbR * 2, thumbR * 2); + + g.setColour (juce::Colour (0xff666677).withAlpha (0.5f).interpolatedWith (onColour.withAlpha (0.6f), glowIntensity)); + g.drawEllipse (thumbX - thumbR, thumbY - thumbR, thumbR * 2, thumbR * 2, 0.8f); + + float hlR = thumbR * 0.4f; + g.setColour (juce::Colours::white.withAlpha (0.1f + 0.15f * glowIntensity)); + g.fillEllipse (thumbX - hlR, thumbY - thumbR * 0.6f - hlR * 0.3f, hlR * 2, hlR * 1.2f); + } +} diff --git a/Source/LookAndFeel.h b/Source/LookAndFeel.h new file mode 100644 index 0000000..0500c12 --- /dev/null +++ b/Source/LookAndFeel.h @@ -0,0 +1,50 @@ +#pragma once +#include + +class InstaLPEQLookAndFeel : public juce::LookAndFeel_V4 +{ +public: + // Colour palette + static inline const juce::Colour bgDark { 0xff1a1a2e }; + static inline const juce::Colour bgMedium { 0xff16213e }; + static inline const juce::Colour bgLight { 0xff0f3460 }; + static inline const juce::Colour textPrimary { 0xffe0e0e0 }; + static inline const juce::Colour textSecondary { 0xff888899 }; + static inline const juce::Colour accent { 0xff00ff88 }; + + // Knob type property key + static constexpr const char* knobTypeProperty = "knobType"; + + InstaLPEQLookAndFeel(); + + void drawRotarySlider (juce::Graphics& g, int x, int y, int width, int height, + float sliderPosProportional, float rotaryStartAngle, + float rotaryEndAngle, juce::Slider& slider) override; + + void drawButtonBackground (juce::Graphics& g, juce::Button& button, + const juce::Colour& backgroundColour, + bool shouldDrawButtonAsHighlighted, + bool shouldDrawButtonAsDown) override; + + void drawToggleButton (juce::Graphics& g, juce::ToggleButton& button, + bool shouldDrawButtonAsHighlighted, + bool shouldDrawButtonAsDown) override; + + // Custom fonts + juce::Font getRegularFont (float height) const; + juce::Font getMediumFont (float height) const; + juce::Font getBoldFont (float height) const; + + // Background texture + void drawBackgroundTexture (juce::Graphics& g, juce::Rectangle area); + + juce::Typeface::Ptr getTypefaceForFont (const juce::Font& font) override; + +private: + juce::Typeface::Ptr typefaceRegular; + juce::Typeface::Ptr typefaceMedium; + juce::Typeface::Ptr typefaceBold; + + juce::Image noiseTexture; + void generateNoiseTexture(); +}; diff --git a/Source/NodeParameterPanel.cpp b/Source/NodeParameterPanel.cpp new file mode 100644 index 0000000..4193357 --- /dev/null +++ b/Source/NodeParameterPanel.cpp @@ -0,0 +1,145 @@ +#include "NodeParameterPanel.h" +#include "LookAndFeel.h" + +NodeParameterPanel::NodeParameterPanel() +{ + setupSlider (freqSlider, freqLabel, 20.0, 20000.0, 1.0, " Hz"); + freqSlider.setSkewFactorFromMidPoint (1000.0); + + setupSlider (gainSlider, gainLabel, -24.0, 24.0, 0.1, " dB"); + + setupSlider (qSlider, qLabel, 0.1, 18.0, 0.01, ""); + qSlider.setSkewFactorFromMidPoint (1.0); + qSlider.getProperties().set (InstaLPEQLookAndFeel::knobTypeProperty, "dark"); + + typeSelector.addItem ("Peak", 1); + typeSelector.addItem ("Low Shelf", 2); + typeSelector.addItem ("High Shelf", 3); + typeSelector.setSelectedId (1, juce::dontSendNotification); + typeSelector.addListener (this); + addAndMakeVisible (typeSelector); + + deleteButton.addListener (this); + addAndMakeVisible (deleteButton); + + addAndMakeVisible (bandInfoLabel); + bandInfoLabel.setJustificationType (juce::Justification::centredLeft); + + setSelectedBand (-1, nullptr); +} + +void NodeParameterPanel::setupSlider (juce::Slider& s, juce::Label& l, double min, double max, double step, const char* suffix) +{ + s.setSliderStyle (juce::Slider::RotaryHorizontalVerticalDrag); + s.setTextBoxStyle (juce::Slider::TextBoxBelow, false, 60, 16); + s.setRange (min, max, step); + s.setTextValueSuffix (suffix); + s.addListener (this); + addAndMakeVisible (s); + + l.setJustificationType (juce::Justification::centred); + addAndMakeVisible (l); +} + +void NodeParameterPanel::setSelectedBand (int index, const EQBand* band) +{ + selectedIndex = index; + updatingFromExternal = true; + + bool hasBand = (index >= 0 && band != nullptr); + freqSlider.setEnabled (hasBand); + gainSlider.setEnabled (hasBand); + qSlider.setEnabled (hasBand); + typeSelector.setEnabled (hasBand); + deleteButton.setEnabled (hasBand); + + if (hasBand) + { + currentBand = *band; + freqSlider.setValue (band->frequency, juce::dontSendNotification); + gainSlider.setValue (band->gainDb, juce::dontSendNotification); + qSlider.setValue (band->q, juce::dontSendNotification); + typeSelector.setSelectedId ((int) band->type + 1, juce::dontSendNotification); + bandInfoLabel.setText ("Band " + juce::String (index + 1), juce::dontSendNotification); + } + else + { + bandInfoLabel.setText ("No band selected", juce::dontSendNotification); + } + + updatingFromExternal = false; + repaint(); +} + +void NodeParameterPanel::paint (juce::Graphics& g) +{ + auto bounds = getLocalBounds().toFloat(); + g.setColour (InstaLPEQLookAndFeel::bgMedium.darker (0.2f)); + g.fillRoundedRectangle (bounds, 4.0f); + g.setColour (InstaLPEQLookAndFeel::bgLight.withAlpha (0.3f)); + g.drawRoundedRectangle (bounds, 4.0f, 1.0f); +} + +void NodeParameterPanel::resized() +{ + auto bounds = getLocalBounds().reduced (6); + float scale = (float) getHeight() / 90.0f; + + auto left = bounds.removeFromLeft (100); + bandInfoLabel.setBounds (left.removeFromTop ((int) (20 * scale))); + + auto typeBounds = left.removeFromTop ((int) (22 * scale)); + typeSelector.setBounds (typeBounds.reduced (2)); + + auto delBounds = left.removeFromTop ((int) (22 * scale)); + deleteButton.setBounds (delBounds.reduced (2)); + + // Knobs take the rest + int knobW = bounds.getWidth() / 3; + int labelH = (int) std::max (14.0f, 16.0f * scale); + + auto freqArea = bounds.removeFromLeft (knobW); + freqLabel.setBounds (freqArea.removeFromTop (labelH)); + freqSlider.setBounds (freqArea); + + auto gainArea = bounds.removeFromLeft (knobW); + gainLabel.setBounds (gainArea.removeFromTop (labelH)); + gainSlider.setBounds (gainArea); + + auto qArea = bounds; + qLabel.setBounds (qArea.removeFromTop (labelH)); + qSlider.setBounds (qArea); +} + +void NodeParameterPanel::sliderValueChanged (juce::Slider* slider) +{ + if (updatingFromExternal || selectedIndex < 0 || listener == nullptr) + return; + + if (slider == &freqSlider) + currentBand.frequency = (float) freqSlider.getValue(); + else if (slider == &gainSlider) + currentBand.gainDb = (float) gainSlider.getValue(); + else if (slider == &qSlider) + currentBand.q = (float) qSlider.getValue(); + + listener->nodeParameterChanged (selectedIndex, currentBand); +} + +void NodeParameterPanel::comboBoxChanged (juce::ComboBox* box) +{ + if (updatingFromExternal || selectedIndex < 0 || listener == nullptr) + return; + + if (box == &typeSelector) + { + currentBand.type = static_cast (typeSelector.getSelectedId() - 1); + listener->nodeParameterChanged (selectedIndex, currentBand); + } +} + +void NodeParameterPanel::buttonClicked (juce::Button* button) +{ + if (button == &deleteButton && selectedIndex >= 0 && listener != nullptr) + listener->nodeDeleteRequested (selectedIndex); +} diff --git a/Source/NodeParameterPanel.h b/Source/NodeParameterPanel.h new file mode 100644 index 0000000..ce87b9f --- /dev/null +++ b/Source/NodeParameterPanel.h @@ -0,0 +1,49 @@ +#pragma once +#include +#include "EQBand.h" + +class NodeParameterPanel : public juce::Component, + private juce::Slider::Listener, + private juce::ComboBox::Listener, + private juce::Button::Listener +{ +public: + struct Listener + { + virtual ~Listener() = default; + virtual void nodeParameterChanged (int bandIndex, const EQBand& band) = 0; + virtual void nodeDeleteRequested (int bandIndex) = 0; + }; + + NodeParameterPanel(); + + void setListener (Listener* l) { listener = l; } + void setSelectedBand (int index, const EQBand* band); + int getSelectedIndex() const { return selectedIndex; } + + void resized() override; + void paint (juce::Graphics& g) override; + +private: + void sliderValueChanged (juce::Slider* slider) override; + void comboBoxChanged (juce::ComboBox* box) override; + void buttonClicked (juce::Button* button) override; + + int selectedIndex = -1; + EQBand currentBand; + bool updatingFromExternal = false; + + juce::Slider freqSlider, gainSlider, qSlider; + juce::Label freqLabel { {}, "FREQ" }; + juce::Label gainLabel { {}, "GAIN" }; + juce::Label qLabel { {}, "Q" }; + juce::Label bandInfoLabel { {}, "No band selected" }; + juce::ComboBox typeSelector; + juce::TextButton deleteButton { "DELETE" }; + + Listener* listener = nullptr; + + void setupSlider (juce::Slider& s, juce::Label& l, double min, double max, double step, const char* suffix); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NodeParameterPanel) +}; diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp new file mode 100644 index 0000000..14b4823 --- /dev/null +++ b/Source/PluginEditor.cpp @@ -0,0 +1,211 @@ +#include "PluginEditor.h" + +InstaLPEQEditor::InstaLPEQEditor (InstaLPEQProcessor& p) + : AudioProcessorEditor (p), processor (p) +{ + setLookAndFeel (&customLookAndFeel); + juce::LookAndFeel::setDefaultLookAndFeel (&customLookAndFeel); + + // Title + titleLabel.setFont (customLookAndFeel.getBoldFont (26.0f)); + titleLabel.setColour (juce::Label::textColourId, InstaLPEQLookAndFeel::accent); + addAndMakeVisible (titleLabel); + + versionLabel.setFont (customLookAndFeel.getRegularFont (13.0f)); + versionLabel.setColour (juce::Label::textColourId, InstaLPEQLookAndFeel::textSecondary); + versionLabel.setJustificationType (juce::Justification::centredRight); + addAndMakeVisible (versionLabel); + + // Bypass + bypassToggle.setToggleState (processor.bypassed.load(), juce::dontSendNotification); + addAndMakeVisible (bypassToggle); + bypassLabel.setFont (customLookAndFeel.getMediumFont (13.0f)); + bypassLabel.setColour (juce::Label::textColourId, InstaLPEQLookAndFeel::textSecondary); + addAndMakeVisible (bypassLabel); + + // EQ curve + curveDisplay.setListener (this); + addAndMakeVisible (curveDisplay); + + // Node panel + nodePanel.setListener (this); + addAndMakeVisible (nodePanel); + + // Master gain + masterGainSlider.setSliderStyle (juce::Slider::RotaryHorizontalVerticalDrag); + masterGainSlider.setTextBoxStyle (juce::Slider::TextBoxBelow, false, 60, 16); + masterGainSlider.setRange (-24.0, 24.0, 0.1); + masterGainSlider.setValue (0.0); + masterGainSlider.setTextValueSuffix (" dB"); + addAndMakeVisible (masterGainSlider); + masterGainLabel.setFont (customLookAndFeel.getMediumFont (13.0f)); + masterGainLabel.setJustificationType (juce::Justification::centred); + addAndMakeVisible (masterGainLabel); + + // Sizing + constrainer.setMinimumSize (700, 450); + constrainer.setMaximumSize (1920, 1080); + setConstrainer (&constrainer); + setResizable (true, true); + setSize (900, 650); + + startTimerHz (30); + syncDisplayFromProcessor(); +} + +InstaLPEQEditor::~InstaLPEQEditor() +{ + setLookAndFeel (nullptr); +} + +void InstaLPEQEditor::paint (juce::Graphics& g) +{ + auto bounds = getLocalBounds().toFloat(); + + // Background gradient + juce::ColourGradient bgGrad (InstaLPEQLookAndFeel::bgDark, 0, 0, + InstaLPEQLookAndFeel::bgDark.darker (0.3f), 0, bounds.getBottom(), false); + g.setGradientFill (bgGrad); + g.fillAll(); + + // Noise texture + customLookAndFeel.drawBackgroundTexture (g, getLocalBounds()); + + // Top header bar + float scale = (float) getHeight() / 650.0f; + int headerH = (int) std::max (28.0f, 36.0f * scale); + g.setColour (InstaLPEQLookAndFeel::bgDark.darker (0.4f)); + g.fillRect (0, 0, getWidth(), headerH); + + g.setColour (InstaLPEQLookAndFeel::bgLight.withAlpha (0.3f)); + g.drawHorizontalLine (headerH, 0.0f, (float) getWidth()); +} + +void InstaLPEQEditor::resized() +{ + auto bounds = getLocalBounds(); + float scale = (float) getHeight() / 650.0f; + + // Top bar + int headerH = (int) std::max (28.0f, 36.0f * scale); + auto header = bounds.removeFromTop (headerH); + int pad = (int) (8 * scale); + header.reduce (pad, 2); + + titleLabel.setFont (customLookAndFeel.getBoldFont (std::max (18.0f, 26.0f * scale))); + titleLabel.setBounds (header.removeFromLeft (200)); + + versionLabel.setFont (customLookAndFeel.getRegularFont (std::max (10.0f, 13.0f * scale))); + versionLabel.setBounds (header.removeFromRight (60)); + + auto bypassArea = header.removeFromRight (80); + bypassLabel.setBounds (bypassArea.removeFromLeft (50)); + bypassToggle.setBounds (bypassArea); + + // Bottom master row + int masterH = (int) std::max (50.0f, 65.0f * scale); + auto masterArea = bounds.removeFromBottom (masterH).reduced (pad, 2); + + // Divider above master + // (painted in paint()) + + masterGainLabel.setFont (customLookAndFeel.getMediumFont (std::max (11.0f, 14.0f * scale))); + auto labelArea = masterArea.removeFromLeft (60); + masterGainLabel.setBounds (labelArea); + masterGainSlider.setBounds (masterArea.removeFromLeft (masterH)); + + // Node parameter panel (15% of remaining height) + int nodePanelH = (int) (bounds.getHeight() * 0.18f); + auto nodePanelArea = bounds.removeFromBottom (nodePanelH).reduced (pad, 2); + nodePanel.setBounds (nodePanelArea); + + // EQ curve display (rest) + auto curveArea = bounds.reduced (pad, 2); + curveDisplay.setBounds (curveArea); +} + +void InstaLPEQEditor::timerCallback() +{ + // Sync bypass + processor.bypassed.store (bypassToggle.getToggleState()); + processor.masterGainDb.store ((float) masterGainSlider.getValue()); + + // Update display with latest magnitude response + auto magDb = processor.getFIREngine().getMagnitudeResponseDb(); + if (! magDb.empty()) + { + curveDisplay.setMagnitudeResponse (magDb, processor.getCurrentSampleRate(), + processor.getFIREngine().getFIRLength()); + } + + // Sync bands from processor (in case of state restore) + auto currentBands = processor.getBands(); + curveDisplay.setBands (currentBands); + + // Update node panel if selected + int sel = curveDisplay.getSelectedBandIndex(); + if (sel >= 0 && sel < (int) currentBands.size()) + nodePanel.setSelectedBand (sel, ¤tBands[sel]); +} + +// ============================================================ +// EQCurveDisplay::Listener +// ============================================================ + +void InstaLPEQEditor::bandAdded (int /*index*/, float freq, float gainDb) +{ + processor.addBand (freq, gainDb); + syncDisplayFromProcessor(); + curveDisplay.setSelectedBand (processor.getNumBands() - 1); +} + +void InstaLPEQEditor::bandRemoved (int index) +{ + processor.removeBand (index); + curveDisplay.setSelectedBand (-1); + syncDisplayFromProcessor(); +} + +void InstaLPEQEditor::bandChanged (int index, const EQBand& band) +{ + processor.setBand (index, band); + // Don't call syncDisplayFromProcessor here to avoid flicker during drag + auto currentBands = processor.getBands(); + curveDisplay.setBands (currentBands); + + if (index == nodePanel.getSelectedIndex() && index < (int) currentBands.size()) + nodePanel.setSelectedBand (index, ¤tBands[index]); +} + +void InstaLPEQEditor::selectedBandChanged (int index) +{ + auto currentBands = processor.getBands(); + if (index >= 0 && index < (int) currentBands.size()) + nodePanel.setSelectedBand (index, ¤tBands[index]); + else + nodePanel.setSelectedBand (-1, nullptr); +} + +// ============================================================ +// NodeParameterPanel::Listener +// ============================================================ + +void InstaLPEQEditor::nodeParameterChanged (int bandIndex, const EQBand& band) +{ + processor.setBand (bandIndex, band); + syncDisplayFromProcessor(); +} + +void InstaLPEQEditor::nodeDeleteRequested (int bandIndex) +{ + processor.removeBand (bandIndex); + curveDisplay.setSelectedBand (-1); + nodePanel.setSelectedBand (-1, nullptr); + syncDisplayFromProcessor(); +} + +void InstaLPEQEditor::syncDisplayFromProcessor() +{ + auto currentBands = processor.getBands(); + curveDisplay.setBands (currentBands); +} diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h new file mode 100644 index 0000000..389c359 --- /dev/null +++ b/Source/PluginEditor.h @@ -0,0 +1,52 @@ +#pragma once +#include +#include "PluginProcessor.h" +#include "LookAndFeel.h" +#include "EQCurveDisplay.h" +#include "NodeParameterPanel.h" + +class InstaLPEQEditor : public juce::AudioProcessorEditor, + private juce::Timer, + private EQCurveDisplay::Listener, + private NodeParameterPanel::Listener +{ +public: + explicit InstaLPEQEditor (InstaLPEQProcessor& p); + ~InstaLPEQEditor() override; + + void paint (juce::Graphics& g) override; + void resized() override; + +private: + void timerCallback() override; + + // EQCurveDisplay::Listener + void bandAdded (int index, float freq, float gainDb) override; + void bandRemoved (int index) override; + void bandChanged (int index, const EQBand& band) override; + void selectedBandChanged (int index) override; + + // NodeParameterPanel::Listener + void nodeParameterChanged (int bandIndex, const EQBand& band) override; + void nodeDeleteRequested (int bandIndex) override; + + void syncDisplayFromProcessor(); + + InstaLPEQProcessor& processor; + InstaLPEQLookAndFeel customLookAndFeel; + + EQCurveDisplay curveDisplay; + NodeParameterPanel nodePanel; + + juce::Label titleLabel { {}, "INSTALPEQ" }; + juce::Label versionLabel { {}, "v1.0" }; + juce::ToggleButton bypassToggle; + juce::Label bypassLabel { {}, "BYPASS" }; + + juce::Slider masterGainSlider; + juce::Label masterGainLabel { {}, "MASTER" }; + + juce::ComponentBoundsConstrainer constrainer; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (InstaLPEQEditor) +}; diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp new file mode 100644 index 0000000..3c4f4d8 --- /dev/null +++ b/Source/PluginProcessor.cpp @@ -0,0 +1,196 @@ +#include "PluginProcessor.h" +#include "PluginEditor.h" + +InstaLPEQProcessor::InstaLPEQProcessor() + : AudioProcessor (BusesProperties() + .withInput ("Input", juce::AudioChannelSet::stereo(), true) + .withOutput ("Output", juce::AudioChannelSet::stereo(), true)) +{ +} + +InstaLPEQProcessor::~InstaLPEQProcessor() +{ + firEngine.stop(); +} + +bool InstaLPEQProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const +{ + if (layouts.getMainInputChannelSet() != juce::AudioChannelSet::stereo() || + layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo()) + return false; + return true; +} + +void InstaLPEQProcessor::prepareToPlay (double sampleRate, int samplesPerBlock) +{ + currentSampleRate = sampleRate; + currentBlockSize = samplesPerBlock; + + juce::dsp::ProcessSpec spec { sampleRate, (juce::uint32) samplesPerBlock, 2 }; + convolution.prepare (spec); + + firEngine.start (sampleRate); + updateFIR(); + + setLatencySamples (firEngine.getLatencySamples()); +} + +void InstaLPEQProcessor::releaseResources() +{ + firEngine.stop(); + convolution.reset(); +} + +void InstaLPEQProcessor::processBlock (juce::AudioBuffer& buffer, juce::MidiBuffer&) +{ + juce::ScopedNoDenormals noDenormals; + + // Check for new FIR from background thread + if (auto newFIR = firEngine.getNewFIR()) + { + convolution.loadImpulseResponse (std::move (*newFIR), currentSampleRate, + juce::dsp::Convolution::Stereo::no, + juce::dsp::Convolution::Trim::no, + juce::dsp::Convolution::Normalise::no); + firLoaded = true; + } + + if (bypassed.load() || ! firLoaded) + return; + + // Process through convolution + juce::dsp::AudioBlock block (buffer); + juce::dsp::ProcessContextReplacing context (block); + convolution.process (context); + + // Apply master gain + float gain = juce::Decibels::decibelsToGain (masterGainDb.load()); + if (std::abs (gain - 1.0f) > 0.001f) + buffer.applyGain (gain); +} + +// ============================================================ +// Band management +// ============================================================ + +std::vector InstaLPEQProcessor::getBands() const +{ + const juce::SpinLock::ScopedLockType lock (bandLock); + return bands; +} + +void InstaLPEQProcessor::setBand (int index, const EQBand& band) +{ + { + const juce::SpinLock::ScopedLockType lock (bandLock); + if (index >= 0 && index < (int) bands.size()) + bands[index] = band; + } + updateFIR(); +} + +void InstaLPEQProcessor::addBand (float freq, float gainDb) +{ + { + const juce::SpinLock::ScopedLockType lock (bandLock); + if ((int) bands.size() >= maxBands) + return; + EQBand b; + b.frequency = freq; + b.gainDb = gainDb; + bands.push_back (b); + } + updateFIR(); +} + +void InstaLPEQProcessor::removeBand (int index) +{ + { + const juce::SpinLock::ScopedLockType lock (bandLock); + if (index >= 0 && index < (int) bands.size()) + bands.erase (bands.begin() + index); + } + updateFIR(); +} + +int InstaLPEQProcessor::getNumBands() const +{ + const juce::SpinLock::ScopedLockType lock (bandLock); + return (int) bands.size(); +} + +void InstaLPEQProcessor::updateFIR() +{ + auto currentBands = getBands(); + firEngine.setBands (currentBands); +} + +// ============================================================ +// State save/restore +// ============================================================ + +void InstaLPEQProcessor::getStateInformation (juce::MemoryBlock& destData) +{ + juce::XmlElement xml ("InstaLPEQ"); + xml.setAttribute ("bypass", bypassed.load()); + xml.setAttribute ("masterGain", (double) masterGainDb.load()); + + auto currentBands = getBands(); + for (int i = 0; i < (int) currentBands.size(); ++i) + { + auto* bandXml = xml.createNewChildElement ("Band"); + bandXml->setAttribute ("freq", (double) currentBands[i].frequency); + bandXml->setAttribute ("gain", (double) currentBands[i].gainDb); + bandXml->setAttribute ("q", (double) currentBands[i].q); + bandXml->setAttribute ("type", (int) currentBands[i].type); + bandXml->setAttribute ("enabled", currentBands[i].enabled); + } + + copyXmlToBinary (xml, destData); +} + +void InstaLPEQProcessor::setStateInformation (const void* data, int sizeInBytes) +{ + auto xml = getXmlFromBinary (data, sizeInBytes); + if (xml == nullptr || ! xml->hasTagName ("InstaLPEQ")) + return; + + bypassed.store (xml->getBoolAttribute ("bypass", false)); + masterGainDb.store ((float) xml->getDoubleAttribute ("masterGain", 0.0)); + + std::vector loadedBands; + for (auto* bandXml : xml->getChildIterator()) + { + if (! bandXml->hasTagName ("Band")) + continue; + + EQBand b; + b.frequency = (float) bandXml->getDoubleAttribute ("freq", 1000.0); + b.gainDb = (float) bandXml->getDoubleAttribute ("gain", 0.0); + b.q = (float) bandXml->getDoubleAttribute ("q", 1.0); + b.type = static_cast (bandXml->getIntAttribute ("type", 0)); + b.enabled = bandXml->getBoolAttribute ("enabled", true); + loadedBands.push_back (b); + } + + { + const juce::SpinLock::ScopedLockType lock (bandLock); + bands = loadedBands; + } + updateFIR(); +} + +// ============================================================ +// Editor +// ============================================================ + +juce::AudioProcessorEditor* InstaLPEQProcessor::createEditor() +{ + return new InstaLPEQEditor (*this); +} + +// This creates new instances of the plugin +juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() +{ + return new InstaLPEQProcessor(); +} diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h new file mode 100644 index 0000000..1ac0da2 --- /dev/null +++ b/Source/PluginProcessor.h @@ -0,0 +1,64 @@ +#pragma once +#include +#include "EQBand.h" +#include "FIREngine.h" + +class InstaLPEQProcessor : public juce::AudioProcessor +{ +public: + static constexpr int maxBands = 8; + + InstaLPEQProcessor(); + ~InstaLPEQProcessor() override; + + void prepareToPlay (double sampleRate, int samplesPerBlock) override; + void releaseResources() override; + void processBlock (juce::AudioBuffer&, juce::MidiBuffer&) override; + + juce::AudioProcessorEditor* createEditor() override; + bool hasEditor() const override { return true; } + + const juce::String getName() const override { return JucePlugin_Name; } + bool acceptsMidi() const override { return false; } + bool producesMidi() const override { return false; } + bool isBusesLayoutSupported (const BusesLayout& layouts) const override; + double getTailLengthSeconds() const override { return 0.0; } + + 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; + + // Band management (called from GUI thread) + std::vector getBands() const; + void setBand (int index, const EQBand& band); + void addBand (float freq, float gainDb); + void removeBand (int index); + int getNumBands() const; + + // Settings + std::atomic bypassed { false }; + std::atomic masterGainDb { 0.0f }; + + FIREngine& getFIREngine() { return firEngine; } + double getCurrentSampleRate() const { return currentSampleRate; } + +private: + std::vector bands; + juce::SpinLock bandLock; + + FIREngine firEngine; + juce::dsp::Convolution convolution; + + double currentSampleRate = 44100.0; + int currentBlockSize = 512; + bool firLoaded = false; + + void updateFIR(); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (InstaLPEQProcessor) +};