diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..61e646b --- /dev/null +++ b/.clang-format @@ -0,0 +1,83 @@ +# Google C/C++ Code Style settings +# https://clang.llvm.org/docs/ClangFormatStyleOptions.html +# Author: Kehan Xue, kehan.xue (at) gmail.com + +Language: Cpp +BasedOnStyle: Google +AccessModifierOffset: -1 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: None +AlignOperands: Align +AllowAllArgumentsOnNextLine: true +AllowAllConstructorInitializersOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: Empty +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never # To avoid conflict, set this "Never" and each "if statement" should include brace when coding +AllowShortLambdasOnASingleLine: Inline +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: true +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterStruct: false + AfterControlStatement: Never + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: false +BreakBeforeBinaryOperators: None +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: BeforeColon +BreakInheritanceList: BeforeColon +ColumnLimit: 80 +CompactNamespaces: false +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false # Make sure the * or & align on the left +EmptyLineBeforeAccessModifier: LogicalBlock +FixNamespaceComments: true +IncludeBlocks: Preserve +IndentCaseLabels: true +IndentPPDirectives: None +IndentWidth: 4 +KeepEmptyLinesAtTheStartOfBlocks: true +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PointerAlignment: Left +ReflowComments: false +# SeparateDefinitionBlocks: Always # Only support since clang-format 14 +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceBeforeSquareBrackets: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: c++11 +TabWidth: 4 +UseTab: Always \ No newline at end of file diff --git a/.github/workflows/cmake-linux-platform.yml b/.github/workflows/cmake-linux-platform.yml new file mode 100644 index 0000000..b3845ec --- /dev/null +++ b/.github/workflows/cmake-linux-platform.yml @@ -0,0 +1,41 @@ +# This starter workflow is for a CMake project running on a single platform. There is a different starter workflow if you need cross-platform coverage. +# See: https://github.com/actions/starter-workflows/blob/main/ci/cmake-multi-platform.yml +name: CMake on a single platform (GNU/Linux) + +on: + push: + branches: [ "dev" ] + pull_request: + branches: [ "dev" ] + +env: + BUILD_TYPE: Release + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: false # Don't fetch submodules + fetch-depth: 1 + + - name: Get JUCE submodule + run: | + git submodule init JUCE + git submodule update --depth 1 JUCE + + - name: Install dependencies + run: | + sudo apt-get update -y --no-install-recommends + sudo apt-get install -y --no-install-recommends libasound2-dev libjack-jackd2-dev ladspa-sdk libcurl4-openssl-dev libfreetype-dev libfontconfig1-dev libx11-dev libxcomposite-dev libxcursor-dev libxext-dev libxinerama-dev libxrandr-dev libxrender-dev libwebkit2gtk-4.1-dev libglu1-mesa-dev mesa-common-dev + + - name: Configure CMake + run: | + mkdir build/ + cd build/ + cmake .. -G "Unix Makefiles" + + - name: Build + run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} diff --git a/.gitignore b/.gitignore index 4a5b5e3..c3678a8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,13 +2,19 @@ .idea/**/tasks.xml .idea/**/usage.statistics.xml .idea/**/dictionaries +.idea/editor.xml # Compiled sources -cmake-build-debug -build-ios +cmake-build-debug/ +build-ios/ +build/ + +# JUCE dependencies +JUCE/ # macOS specific .DS_Store # Windows specific Thumbs.db +/.idea/vcs.xml diff --git a/.gitmodules b/.gitmodules index 8851e64..cb168bc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "docs"] - path = docs - url = git@github.com:k147-studio/docs.git [submodule "JUCE"] path = JUCE - url = https://github.com/juce-framework/JUCE.git + url = https://github.com/k147-studio/JUCE.git diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/editor.xml b/.idea/editor.xml deleted file mode 100644 index e62c182..0000000 --- a/.idea/editor.xml +++ /dev/null @@ -1,583 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 79b3c94..2bc1fda 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,6 @@ - + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 154f836..480f52b 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,7 +1,8 @@ - + + \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index dfc0798..6160d8a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,15 +31,11 @@ juce_add_gui_app(kAmp # DOCUMENT_EXTENSIONS ... # Specify file extensions that should be associated with this app BUNDLE_ID = "eu.k-147.kAmp" COMPANY_NAME "K-147" - PRODUCT_NAME "kAmp") + PRODUCT_NAME "kAmp" + ICON_BIG "resources/images/kamp_logo_with_background.png" + ICON_SMALL "resources/images/kamp_logo_with_background.png") -# `juce_generate_juce_header` will create a JuceHeader.h for a given target, which will be generated -# into your build tree. This should be included with `#include `. The include path for -# this header will be automatically added to the target. The main function of the JuceHeader is to -# include all your JUCE module headers; if you're happy to include module headers directly, you -# probably don't need to call this. - -# juce_generate_juce_header(kAmp) +juce_generate_juce_header(kAmp) # `target_sources` adds source files to a target. We pass the target that needs the sources as the # first argument, then a visibility parameter for the sources which should normally be PRIVATE. @@ -53,6 +49,25 @@ file( src/*.cpp ) +if(APPLE) + # Sur macOS, copie dans le bundle app (Contents/Resources) + add_custom_command(TARGET kAmp POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_SOURCE_DIR}/resources + $/../Resources/resources + ) +else() + # Sur Linux/Windows, copie dans le dossier de build à côté de l'exe + add_custom_command(TARGET kAmp POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_SOURCE_DIR}/resources + $/resources + ) +endif() + + + + target_sources(kAmp PRIVATE ${SOURCES}) @@ -73,7 +88,7 @@ target_compile_definitions(kAmp # JUCE_WEB_BROWSER and JUCE_USE_CURL would be on by default, but you might not need them. JUCE_WEB_BROWSER=0 # If you remove this, add `NEEDS_WEB_BROWSER TRUE` to the `juce_add_gui_app` call JUCE_USE_CURL=0 # If you remove this, add `NEEDS_CURL TRUE` to the `juce_add_gui_app` call - JUCE_IOS=1 + # JUCE_IOS=1 JUCE_APPLICATION_NAME_STRING="$" JUCE_APPLICATION_VERSION_STRING="$") @@ -95,7 +110,10 @@ target_compile_definitions(kAmp target_link_libraries(kAmp PRIVATE # GuiAppData # If we'd created a binary data target, we'd link to it here + juce::juce_audio_basics + juce::juce_audio_utils juce::juce_gui_extra + juce::juce_dsp PUBLIC juce::juce_recommended_config_flags juce::juce_recommended_lto_flags diff --git a/JUCE b/JUCE index 51a8a6d..d6181bd 160000 --- a/JUCE +++ b/JUCE @@ -1 +1 @@ -Subproject commit 51a8a6d7aeae7326956d747737ccf1575e61e209 +Subproject commit d6181bde38d858c283c3b7bf699ce6340c050b5d diff --git a/README.md b/README.md index b7dfba9..f39e55b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # kAmp +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/a990f176aa0246e4ad5cffbea1b5e019)](https://app.codacy.com/gh/k147-studio/kAmp/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) + ## How to build ### Prerequisites diff --git a/docs b/docs deleted file mode 160000 index 80463c5..0000000 --- a/docs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 80463c5e6fd68b4b4966701ce6b1dfa24e52da98 diff --git a/include/AbstractEffect.h b/include/AbstractEffect.h new file mode 100644 index 0000000..03437a6 --- /dev/null +++ b/include/AbstractEffect.h @@ -0,0 +1,123 @@ +#pragma once +#include +#include "IEffect.h" +#include + +/** + * @brief Abstract class that represents an effect by determining + * the minimum caracteristics an effect should have. + * Implements the IEffect interface. + */ +class AbstractEffect : public IEffect { + public: + /** + * @brief The name of the effect. + */ + std::string effectName; + + /** + * Tells if the effect is enabled or not. + */ + bool* isEnabled = new bool(true); + + /** + * @brief Initializes a new instance of the AbstractEffect class. + */ + AbstractEffect() = default; + + /** + * @brief Destroys the instance of the AbstractEffect class. + */ + ~AbstractEffect() override = default; + + /** + * @brief Gets the id of the effect. + * @return The id of the effect. + */ + int getId(); + + /** + * @brief Gets the name of the author of the effect. + * @return The name of the author of the effect. + */ + std::string getAuthor(); + + /** + * @brief Gets the description of the effect. + * @return The description of the effect. + */ + std::string getDescription(); + + /** + * @brief Gets the version of the effect. + * @return The version of the effect. + */ + std::string getVersion(); + + /** + * @brief Compares the effect with another given effect. + * @param effect The effect to compare with. + * @return True if the effect is equal to the given effect, false otherwise. + */ + virtual bool operator==(const AbstractEffect* effect) = 0; + + /** + * @brief Applies the effect to the given audio buffer. + * @param bufferToFill The audio buffer to apply the effect to. + */ + void apply(const juce::AudioSourceChannelInfo &bufferToFill) override = 0; + + /** + * @brief Serializes the effect to a JSON object. + * @return JSON object containing serialized effect data. + */ + [[nodiscard]] virtual var toJSON() const { + const auto obj = new DynamicObject(); + obj->setProperty("id", id); + obj->setProperty("author", var(author)); + obj->setProperty("description", var(description)); + obj->setProperty("version", var(version)); + obj->setProperty("type", getEffectType()); + return obj; + } + + /** + * @brief Deserializes the effect from a JSON object. + * @param json JSON object containing serialized effect data. + */ + virtual void fromJSON(const var &json) { + if (const auto *obj = json.getDynamicObject()) { + id = obj->getProperty("id"); + author = obj->getProperty("author").toString().toStdString(); + description = obj->getProperty("description").toString().toStdString(); + version = obj->getProperty("version").toString().toStdString(); + } + } + + /** + * @brief Gets the type name of the effect for serialization purposes. + * @return A string representing the effect type. + */ + [[nodiscard]] virtual String getEffectType() const = 0; + + private: + /** + * @brief The id of the effect. + */ + int id; + + /** + * @brief The name of the author of the effect. + */ + std::string author; + + /** + * @brief The description of the effect. + */ + std::string description; + + /** + * @brief The version of the effect. + */ + std::string version; +}; \ No newline at end of file diff --git a/include/AccountComponent.h b/include/AccountComponent.h new file mode 100644 index 0000000..bd2756a --- /dev/null +++ b/include/AccountComponent.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +class AccountComponent : public Component { +public: + AccountComponent(); + void paint(Graphics&) override; + void resized() override; + +private: + void apiResponseReceived(const String& content); + void saveSettings(); + void importSettings(); + + void setupLabels(); + void setupButtons(); + void setupGrid(); + + Label titleLabel{"titleLabel", "Mon compte"}; + Label responseLabel{"responseLabel", "En attente de réponse..."}; + + Label emailLabel{"emailLabel", "Adresse mail :"}; + Label usernameLabel{"usernameLabel", "Nom d'utilisateur :"}; + Label emailValueLabel{"emailValueLabel", "utilisateur@example.com"}; + Label usernameValueLabel{"usernameValueLabel", "NomUtilisateur"}; + + TextButton saveButton{"Sauvegarder les réglages"}; + TextButton importButton{"Importer les réglages"}; + + Grid grid; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AccountComponent) +}; \ No newline at end of file diff --git a/include/ApiClient.h b/include/ApiClient.h new file mode 100644 index 0000000..d3c7f0a --- /dev/null +++ b/include/ApiClient.h @@ -0,0 +1,8 @@ +#pragma once + +class ApiClient { +public: + using ReplyFunc = std::function; + + static void runHTTP(const URL &url, const ReplyFunc &replyFunc); +}; diff --git a/include/AudioBuffer.h b/include/AudioBuffer.h new file mode 100644 index 0000000..554d259 --- /dev/null +++ b/include/AudioBuffer.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +class AudioBuffer { + public: + AudioBuffer(); + ~AudioBuffer(); + juce::AudioSourceChannelInfo getBuffer(); + private: + juce::AudioSourceChannelInfo bufferToFill; +}; \ No newline at end of file diff --git a/include/BasePedalComponent.h b/include/BasePedalComponent.h new file mode 100644 index 0000000..3997706 --- /dev/null +++ b/include/BasePedalComponent.h @@ -0,0 +1,94 @@ +#pragma once +#include "EffectComponent.h" +#include "PedalPowerIndicatorComponent.h" +#include "PedalSettingsLayoutComponent.h" + +/** + * @brief Base class for pedal components. + * This class provides a common interface and functionality for all pedal components. + */ +class BasePedalComponent : public EffectComponent { +public: + /** + * @brief Initializes a new instance of the BasePedalComponent class. + * @param effect The effect associated with this pedal component. + */ + BasePedalComponent(AbstractEffect* effect); + + /** + * @brief Destroys the instance of the BasePedalComponent class. + */ + ~BasePedalComponent() override; + + /** + * @brief Paints the component. + * @param g The JUCE graphics context that paints the component. + */ + void paint(Graphics& g) override; + + /** + * @brief Resizes the component. + */ + void resized() override; + +protected: + /** + * @brief The default width of the component. + */ + const float DEFAULT_WIDTH = 200.0f; + + /** + * @brief The default height of the component. + */ + const float DEFAULT_HEIGHT = 300.0f; + + /** + * @brief The main layout for the pedal component. + */ + Grid pedalLayout; + + /** + * @brief The primary color of the pedal component. Used as background color. + */ + Colour primaryColor; + + /** + * @brief The secondary color of the pedal component. Used as background color. + */ + Colour secondaryColor; + + /** + * @brief The layout for the pedal settings. + */ + PedalSettingsLayoutComponent* settingsLayout; + + /** + * @brief The label for the pedal name. + */ + Label* pedalLabel; + + /** + * @brief The button to enable/disable the pedal. + */ + ImageButton enablePedalButton; + + /** + * @brief The indicator for the pedal power status. + */ + PedalPowerIndicatorComponent* isEnabledIndicator; + + /** + * @brief Tells if the pedal is enabled or not. + */ + bool* isEnabled; + + /** + * @brief Called when the enable button is clicked. + */ + void onEnableButtonClicked(); + + /** + * @brief Initializes the pedal layout to display it. + */ + void initializePedal(); +}; diff --git a/include/BottomMenuBarComponent.h b/include/BottomMenuBarComponent.h new file mode 100644 index 0000000..c8edfcb --- /dev/null +++ b/include/BottomMenuBarComponent.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +/** + * @brief Represents a graphical component that contains and displays the bottom menu bar. + */ +class BottomMenuBarComponent : public juce::Component { + public: + /** + * @brief Initializes a new instance of the BottomMenuBarComponent class. + */ + BottomMenuBarComponent(); + + /** + * @brief Destroys the instance of the BottomMenuBarComponent class. + */ + ~BottomMenuBarComponent() override; + + /** + * @brief Determines how to display the component. + * @param g The JUCE graphics context that paints the component. + */ + void paint(juce::Graphics &g) override; + + /** + * @brief Determines what to do when the component is resized. + */ + void resized() override; + private: + /** + * @brief The menu bar component that contains the menu items. + */ + juce::MenuBarComponent menuBarComponent; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(BottomMenuBarComponent) +}; \ No newline at end of file diff --git a/include/ChorusEffect.h b/include/ChorusEffect.h new file mode 100644 index 0000000..3382ba3 --- /dev/null +++ b/include/ChorusEffect.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include "AbstractEffect.h" +#include + +class ChorusEffect : public AbstractEffect { +public: + ChorusEffect(); + ~ChorusEffect() override = default; + + void apply(const juce::AudioSourceChannelInfo &bufferToFill) override; + bool operator==(const AbstractEffect* effect) override; + + [[nodiscard]] juce::String getEffectType() const override { return "ChorusEffect"; } + + [[nodiscard]] juce::var toJSON() const override; + void fromJSON(const juce::var &json) override; + + float getDepth() const; + float getRate() const; + float getMix() const; + + void setRate(float newRate); + void setDepth(float newDepth); + void setCentreDelay(float newCentreDelay); + void setFeedback(float newFeedback); + void setMix(float newMix); + void prepare(double sampleRate, int samplesPerBlock, int numChannels); + + +private: + juce::dsp::Chorus chorus; + + float rate = 1.5f; + float depth = 0.5f; + float centreDelay = 7.0f; + float feedback = 0.2f; + float mix = 0.5f; + + void updateParameters(); +}; diff --git a/include/ChorusEffectComponent.h b/include/ChorusEffectComponent.h new file mode 100644 index 0000000..07dcd38 --- /dev/null +++ b/include/ChorusEffectComponent.h @@ -0,0 +1,20 @@ +#pragma once + +#include "BasePedalComponent.h" +#include "ChorusEffect.h" +#include + +class ChorusEffectComponent : public BasePedalComponent +{ +public: + explicit ChorusEffectComponent(AbstractEffect* e); + + +private: + ChorusEffect* chorusEffect; + + juce::Slider depthSlider, rateSlider, mixSlider; + juce::Label depthLabel, rateLabel, mixLabel; + + juce::Grid grid; +}; diff --git a/include/DelayEffect.h b/include/DelayEffect.h new file mode 100644 index 0000000..06d00bd --- /dev/null +++ b/include/DelayEffect.h @@ -0,0 +1,94 @@ +#pragma once + +#include +#include "AbstractEffect.h" + +/** + * @brief Represents a delay effect that applies a delay to the audio stream. + * Inherits from the AbstractEffect class. + */ +class DelayEffect : public AbstractEffect { +public: + /** + * @brief Initializes a new instance of the DelayEffect class. + */ + DelayEffect(); + + /** + * @brief Destroys the instance of the DelayEffect class. + */ + ~DelayEffect() override; + + /** + * @brief Applies the delay effect to the given audio buffer. + * @param bufferToFill The audio buffer to apply the effect to. + */ + void apply(const AudioSourceChannelInfo& bufferToFill) override; + + /** + * @brief Sets the rate of the delay effect. + * @param rate The rate of the delay effect. + */ + void setRate(float rate); + + /** + * @brief Sets the delay of the effect. + * @param delay The delay of the effect. + */ + void setDelay(float delay); + + /** + * @brief Compares the effect with another given effect. + * @param effect The effect to compare with. + * @return True if the effect is equal to the given effect, false otherwise. + */ + bool operator==(const AbstractEffect* effect) override; + + /** + * @brief Gets the type name of the effect for serialization purposes. + * @return A string representing the effect type. + */ + [[nodiscard]] String getEffectType() const override { + return "DelayEffect"; + } + + /** + * @brief Serializes the delay effect to a JSON object. + * @return JSON object containing serialized effect data. + */ + [[nodiscard]] var toJSON() const override { + auto obj = AbstractEffect::toJSON(); + if (auto* dynamicObj = obj.getDynamicObject()) { + dynamicObj->setProperty("rate", rate); + dynamicObj->setProperty("delay", delay); + } + return obj; + } + + /** + * @brief Deserializes the delay effect from a JSON object. + * @param json JSON object containing serialized effect data. + */ + void fromJSON(const var& json) override { + AbstractEffect::fromJSON(json); + + if (const auto* obj = json.getDynamicObject()) { + rate = static_cast(obj->getProperty("rate")); + delay = static_cast(obj->getProperty("delay")); + } + } + +private: + /** + * @brief The rate of the delay. + */ + float rate; + + /** + * @brief The delay of the effect. + */ + float delay; + + std::vector circularBuffer; + int writePosition = 0; +}; diff --git a/include/DelayEffectComponent.h b/include/DelayEffectComponent.h new file mode 100644 index 0000000..627008a --- /dev/null +++ b/include/DelayEffectComponent.h @@ -0,0 +1,57 @@ +#pragma once + +#include + +#include "BasePedalComponent.h" +#include "DelayEffect.h" + +/** + * @brief Represents a graphical component that contains and displays a delay effect. + */ +class DelayEffectComponent : public BasePedalComponent { + public: + /** + * @brief Initializes a new instance of the DelayComponent class. + */ + DelayEffectComponent(); + + /** + * @brief Initializes a new instance of the DelayComponent class with the given AbstractEffect. + * @param effect The effect to initialize the component with. + */ + explicit DelayEffectComponent(AbstractEffect* effect); + + /** + * @brief Destroys the instance of the DelayComponent class. + */ + ~DelayEffectComponent() override; + + private: + /** + * @brief The grid that contains the subcomponents of the delay effect + * and determines their layout. + */ + juce::Grid grid; + + /** + * @brief The slider that controls the rate of the delay effect. + */ + juce::Slider rateSlider; + + /** + * @brief The label of the rate slider. + */ + juce::Label rateLabel; + + /** + * @brief The slider that controls the delay of the delay effect. + */ + juce::Slider delaySlider; + + /** + * @brief The label of the delay slider. + */ + juce::Label delayLabel; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(DelayEffectComponent) +}; \ No newline at end of file diff --git a/include/DistortionEffect.h b/include/DistortionEffect.h new file mode 100644 index 0000000..6f68921 --- /dev/null +++ b/include/DistortionEffect.h @@ -0,0 +1,94 @@ +#pragma once + +#include +#include +#include "AbstractEffect.h" + +class DistortionEffect : public AbstractEffect { +public: + DistortionEffect(); + ~DistortionEffect() override; + + void apply(const AudioSourceChannelInfo &bufferToFill) override; + void prepare(const juce::dsp::ProcessSpec& spec); + void reset() noexcept; + + // Paramètres Boss DS-2 + void setLevel(float value); // 0.0 à 1.0 + void setTone(float value); // 0.0 à 1.0 + void setDist(float value); // 0.0 à 1.0 + void setTurbo(bool enabled); + + float getLevel() const; + float getTone() const; + float getDist() const; + bool isTurbo() const; + + // (Sérialisation, etc.) + /** + * @brief Gets the type name of the effect for serialization purposes. + * @return A string representing the effect type. + */ + [[nodiscard]] String getEffectType() const override { return "DistortionEffect"; } + + /** + * @brief Serializes the delay effect to a JSON object. + * @return JSON object containing serialized effect data. + */ + [[nodiscard]] var toJSON() const override { + auto obj = AbstractEffect::toJSON(); + if (auto *dynamicObj = obj.getDynamicObject()) { + dynamicObj->setProperty("level", level); + dynamicObj->setProperty("tone", tone); + dynamicObj->setProperty("dist", dist); + dynamicObj->setProperty("turbo", turbo); + } + return obj; + } + + /** + * @brief Deserializes the delay effect from a JSON object. + * @param json JSON object containing serialized effect data. + */ + void fromJSON(const var &json) override { + AbstractEffect::fromJSON(json); + if (const auto *obj = json.getDynamicObject()) { + if (obj->hasProperty("level")) level = static_cast(obj->getProperty("level")); + if (obj->hasProperty("tone")) tone = static_cast(obj->getProperty("tone")); + if (obj->hasProperty("dist")) dist = static_cast(obj->getProperty("dist")); + if (obj->hasProperty("turbo")) turbo = static_cast(obj->getProperty("turbo")); + updateWaveshaper(); + updateTone(); + processorChain.get<3>().setGainLinear(level); + } + } + + bool operator==(const AbstractEffect* other) + { + return dynamic_cast(other) && + level == static_cast(other)->level && + tone == static_cast(other)->tone && + dist == static_cast(other)->dist && + turbo == static_cast(other)->turbo; + } + +private: + float level = 1.0f; + float tone = 0.5f; + float dist = 0.5f; + bool turbo = false; + float currentDrive = 1.0f; + bool currentTurbo = false; + + juce::dsp::ProcessorChain< + juce::dsp::IIR::Filter, // High-pass + juce::dsp::WaveShaper, // Distorsion + juce::dsp::IIR::Filter, // Tone (low-pass) + juce::dsp::Gain // Level + > processorChain; + + void updateTone(); + + static float waveshaperFunc(float x); + void updateWaveshaper(); +}; \ No newline at end of file diff --git a/include/DistortionEffectComponent.h b/include/DistortionEffectComponent.h new file mode 100644 index 0000000..450abc9 --- /dev/null +++ b/include/DistortionEffectComponent.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include "BasePedalComponent.h" +#include "ToggleButtonComponent.h" + +class DistortionEffectComponent : public BasePedalComponent { +public: + DistortionEffectComponent(); + explicit DistortionEffectComponent(AbstractEffect* effect); + ~DistortionEffectComponent() override; + +private: + juce::Grid grid; + juce::Slider levelSlider, toneSlider, distSlider; + juce::Label levelLabel, toneLabel, distLabel; + ToggleButtonComponent turboButton; + juce::Label turboLabel; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(DistortionEffectComponent) +}; \ No newline at end of file diff --git a/include/EffectComponent.h b/include/EffectComponent.h new file mode 100644 index 0000000..b57b7fe --- /dev/null +++ b/include/EffectComponent.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include "AbstractEffect.h" + +/** + * @brief Abstract class that represents what an effect component should have. + */ +class EffectComponent : public juce::Component +{ + public: + /** + * @brief Gets the AbstractEffect instance of the component. + * @return The AbstractEffect instance of the component. + */ + AbstractEffect* getEffect(); + protected: + /** + * @brief Initializes a new instance of the EffectComponent class. + * @param effect The effect to initialize the component with. + */ + explicit EffectComponent(AbstractEffect* effect); + + /** + * @brief Destroys the instance of the EffectComponent class. + */ + ~EffectComponent() override = default; + + /** + * @brief The effect that the component represents. + */ + AbstractEffect* effect; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(EffectComponent) +}; \ No newline at end of file diff --git a/include/EffectComponentFactory.h b/include/EffectComponentFactory.h new file mode 100644 index 0000000..6418829 --- /dev/null +++ b/include/EffectComponentFactory.h @@ -0,0 +1,18 @@ +#pragma once + +#include "EffectComponent.h" +#include "AbstractEffect.h" + +/** + * @brief Factory class that creates graphical effect components. + */ +class EffectComponentFactory +{ + public: + /** + * @brief Creates an effect component for the given AbstractEffect. + * @param effect The effect to create the component for. + * @return The created effect component. + */ + static EffectComponent* CreateEffectComponent(AbstractEffect* effect); +}; \ No newline at end of file diff --git a/include/EffectsFactory.h b/include/EffectsFactory.h new file mode 100644 index 0000000..fba24a3 --- /dev/null +++ b/include/EffectsFactory.h @@ -0,0 +1,15 @@ +#pragma once +#include "AbstractEffect.h" + +/** + * @brief A factory class for creating instances of various effects. + */ +class EffectsFactory +{ +public: + /** + * @brief Creates one instance of each effect type. + * @return The vector containing pointers to the created effect instances. + */ + static std::vector createAllEffects(); +}; diff --git a/include/EqualizerEffect.h b/include/EqualizerEffect.h new file mode 100644 index 0000000..37334aa --- /dev/null +++ b/include/EqualizerEffect.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include "AbstractEffect.h" +#include + + +class EqualizerEffect : public AbstractEffect { +public: + EqualizerEffect(); + ~EqualizerEffect() override = default; + + void apply(const AudioSourceChannelInfo &bufferToFill) override; + void setGain(int bandIndex, float gain); + float getGain(int bandIndex) const; + bool operator==(const AbstractEffect* effect) override; + + [[nodiscard]] String getEffectType() const override { return "EqualizerEffect"; } + + [[nodiscard]] var toJSON() const override; + void fromJSON(const var &json) override; + +private: + static constexpr int numBands = 10; + float bandGains[numBands]; // Gain en dB pour chaque bande + std::vector> filters; + + void updateFilters(); +}; diff --git a/include/EqualizerEffectComponent.h b/include/EqualizerEffectComponent.h new file mode 100644 index 0000000..32ecbe1 --- /dev/null +++ b/include/EqualizerEffectComponent.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include "BasePedalComponent.h" +#include "EqualizerEffect.h" + +class EqualizerEffectComponent : public BasePedalComponent { +public: + explicit EqualizerEffectComponent(AbstractEffect* effect); + ~EqualizerEffectComponent() override = default; + +private: + EqualizerEffect* eqEffect; + Slider sliders[10]; + Label labels[10]; + + Grid grid; + + void sliderValueChanged(Slider* slider); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(EqualizerEffectComponent) +}; diff --git a/include/IEffect.h b/include/IEffect.h new file mode 100644 index 0000000..1162255 --- /dev/null +++ b/include/IEffect.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +/** + * @brief Interface that declares the actions an effect should be able to do. + */ +class IEffect { + public: + /** + * @brief Applies the effect to the given audio buffer. + * @param bufferToFill The audio buffer to apply the effects to. + */ + virtual void apply(const AudioSourceChannelInfo &bufferToFill) = 0; + + /** + * @brief Destroys the current instance of IEffect. + */ + virtual ~IEffect() = default; +}; \ No newline at end of file diff --git a/include/LoginComponent.h b/include/LoginComponent.h new file mode 100644 index 0000000..2d545cb --- /dev/null +++ b/include/LoginComponent.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +class LoginComponent final : public Component { +public: + LoginComponent(); + ~LoginComponent() override; + + void paint(Graphics&) override; + void resized() override; + +private: + void setupFields(); + void setupButtons(); + void layoutComponents(); + void skipButtonClicked(); + + TextEditor usernameField; + TextEditor passwordField; + TextButton loginButton{"Login"}; + TextButton skipButton{"Skip"}; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(LoginComponent) +}; \ No newline at end of file diff --git a/include/MainComponent.h b/include/MainComponent.h index 7bde5e1..645f75f 100644 --- a/include/MainComponent.h +++ b/include/MainComponent.h @@ -1,29 +1,104 @@ #pragma once +#include "BottomMenuBarComponent.h" +#include "LoginComponent.h" +#include "Manager.h" +#include "PedalboardComponent.h" +#include "TopMenuBarComponent.h" + // CMake builds don't use an AppConfig.h, so it's safe to include juce module headers // directly. If you need to remain compatible with Projucer-generated builds, and // have called `juce_generate_juce_header()` in your CMakeLists.txt, // you could `#include ` here instead, to make all your module headers visible. -#include - -//============================================================================== -/* - This component lives inside our window, and this is where you should put all - your controls and content. -*/ -class MainComponent final : public juce::Component -{ +#include +#include +#include + + +/** + * @brief The main component of the application that contains all the main UI. + * It is responsible for the layout of the components and the audio processing. + * It contains the pedalboard, the top menu bar, the bottom menu bar and the connection component. + */ +class MainComponent final : public AudioAppComponent { public: - //============================================================================== - MainComponent(); + explicit MainComponent(const Manager &manager); + + /** + * Determines how the component is displayed. + */ + void paint(Graphics &) override; - //============================================================================== - void paint (juce::Graphics&) override; + /** + * Determines what to do when the component is resized. + * Sets the sizes of the subcomponents. + */ void resized() override; + /** + * @brief Prepares the application to process audio. + * @param samplesPerBlockExpected The number of samples per processed block expected. + * @param sampleRate The expected sample rate. + */ + void prepareToPlay(int samplesPerBlockExpected, double sampleRate) override; + + /** + * Releases the audio sources used when the application is closed. + * It avoids audio glitches on the operating system that can happen + * when some resources are not released. + */ + void releaseResources() override; + + /** + * @brief Gets the next audio block to process. + * @param bufferToFill The audio buffer to apply the effects to. + */ + void getNextAudioBlock(const AudioSourceChannelInfo &bufferToFill) override; + private: - //============================================================================== - // Your private member variables go here... - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent) + ImageComponent backgroundImage; + + /** + * The grid that contains the main components on the main page. + */ + Grid grid; + + /** + * The component that allows the user to login. + */ + LoginComponent connectionComponent; + + /** + * Component that contains the pedalboard and that automatically adjust rendering so it becomes + * scrollable if necessary. + */ + juce::Viewport pedalboardContainer; + + /** + * The component that displays the pedalboard. + */ + PedalboardComponent pedalboardComponent; + + /** + * The menu bar at the top of the application. + */ + TopMenuBarComponent topMenuBarComponent; + + /** + * The bottom menu bar of the application. + */ + BottomMenuBarComponent bottomMenuBarComponent; + + /** + * The manager that processes the audio stream and effects. + */ + Manager manager; + + /** + * Tells if the sound is muted or not. + */ + bool isSoundMuted = false; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainComponent) }; diff --git a/include/Manager.h b/include/Manager.h new file mode 100644 index 0000000..3f290dc --- /dev/null +++ b/include/Manager.h @@ -0,0 +1,51 @@ +#pragma once + +#include "Pedalboard.h" + +/** + * @brief Manages the effects in currently used in the pedalboard. + */ +class Manager { +public: + explicit Manager(AbstractEffect *pedalboard); + + /** + * @brief Applies the effects from the pedalboard to the given audio buffer. + * @param bufferToFill The audio buffer to apply the effects to. + */ + void apply(const AudioSourceChannelInfo &bufferToFill) const; + + /** + * @brief Imports a pedalboard. + */ + void importF() const; + + /** + * @brief Adds the given effect at the end of the pedalboard. + * @param effect The effect to append in the pedalboard. + */ + void append(AbstractEffect *effect) const; + + /** + * @brief Exports all the pedalboard. + */ + void exportAll() const; + + /** + * Exports a given list of effects from the pedalboard. + */ + void exportSelection() const; + + /** + * @brief Gets the pedalboard. + * @return The pedalboard. + */ + [[nodiscard]] + AbstractEffect *getPedalboard() const; + +private: + /** + * @brief The pedalboard that contains all the currently used effects. + */ + AbstractEffect *pedalboard; +}; diff --git a/include/ModalOverlayComponent.h b/include/ModalOverlayComponent.h new file mode 100644 index 0000000..ea25545 --- /dev/null +++ b/include/ModalOverlayComponent.h @@ -0,0 +1,20 @@ +#pragma once +#include + +class ModalOverlayComponent : public juce::Component +{ +public: + ModalOverlayComponent(std::string viewName, juce::Component* modalContent); + ~ModalOverlayComponent() override = default; + void resized() override; + void paint(Graphics& g) override; + +private: + juce::Label viewNameLabel; + juce::ImageButton closeOverlayButton; + juce::Component* modalComponent = nullptr; + + void onCloseOverlayButtonClicked(); + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ModalOverlayComponent) +}; + diff --git a/include/NoiseGateEffect.h b/include/NoiseGateEffect.h new file mode 100644 index 0000000..7501c14 --- /dev/null +++ b/include/NoiseGateEffect.h @@ -0,0 +1,100 @@ +#pragma once +#include "AbstractEffect.h" + +/** + * @brief Represents a noise gate effect that applies a noise gate to the audio stream. + * Inherits from the AbstractEffect class. + */ +class NoiseGateEffect: public AbstractEffect +{ +public: + /** + * @brief Initializes a new instance of the NoiseGateEffect class. + */ + NoiseGateEffect(); + + /** + * @brief Destroys the instance of the NoiseGateEffect class. + */ + ~NoiseGateEffect() override; + + /** + * @brief Applies the noise gate effect to the given audio buffer. + * @param bufferToFill The audio buffer to apply the effect to. + */ + void apply(const AudioSourceChannelInfo &bufferToFill) override; + + /** + * @brief Sets the threshold of the noise gate effect. + * @param threshold The threshold of the noise gate effect. + */ + void setThreshold(float threshold); + + /** + * @brief Sets the attack time of the noise gate effect. + * @param attack The attack time of the noise gate effect. + */ + void setAttack(float attack); + + /** + * @brief Sets the release time of the noise gate effect. + * @param release The release time of the noise gate effect. + */ + void setRelease(float release); + + /** + * @brief Compares the effect with another given effect. + * @param effect The effect to compare with. + * @return True if the effect is equal to the given effect, false otherwise. + */ + bool operator==(const AbstractEffect* effect) override; + + /** + * @brief Gets the type name of the effect for serialization purposes. + * @return A string representing the effect type. + */ + [[nodiscard]] String getEffectType() const override { return "NoiseGateEffect"; } + + /** + * @brief Serializes the delay effect to a JSON object. + * @return JSON object containing serialized effect data. + */ + [[nodiscard]] var toJSON() const override { + auto obj = AbstractEffect::toJSON(); + if (auto *dynamicObj = obj.getDynamicObject()) { + dynamicObj->setProperty("threshold", threshold); + dynamicObj->setProperty("attack", attack); + dynamicObj->setProperty("release", release); + } + return obj; + } + + /** + * @brief Deserializes the delay effect from a JSON object. + * @param json JSON object containing serialized effect data. + */ + void fromJSON(const var &json) override { + AbstractEffect::fromJSON(json); + + if (const auto *obj = json.getDynamicObject()) { + threshold = static_cast(obj->getProperty("threshold")); + attack = static_cast(obj->getProperty("attack")); + release = static_cast(obj->getProperty("release")); + } + } +private: + /** + * @brief The threshold of the noise gate effect. + */ + float threshold; + + /** + * @brief The attack time of the noise gate effect. + */ + float attack; + + /** + * @brief The release time of the noise gate effect. + */ + float release; +}; diff --git a/include/NoiseGateEffectComponent.h b/include/NoiseGateEffectComponent.h new file mode 100644 index 0000000..e1e9849 --- /dev/null +++ b/include/NoiseGateEffectComponent.h @@ -0,0 +1,75 @@ +#pragma once +#include "BasePedalComponent.h" +#include "NoiseGateEffect.h" +#include + +/** + * @brief Represents a noise gate effect component that applies a noise gate to the audio stream. + * Inherits from the BasePedalComponent class. + */ +class NoiseGateEffectComponent: public BasePedalComponent +{ +public: + /** + * @brief Initializes a new instance of the NoiseGateEffectComponent class. + */ + explicit NoiseGateEffectComponent(AbstractEffect* effect); + + /** + * @brief Destroys the instance of the NoiseGateEffectComponent class. + */ + ~NoiseGateEffectComponent() override; +private: + /** + * @brief The Grid component that contains the menu items. + */ + juce::Grid grid; + + /** + * @brief The slider that manages the threshold value. + */ + juce::Slider thresholdSlider; + + /** + * @brief The label that is linked to the threshold slider. + */ + juce::Label thresholdLabel; + + /** + * @brief The slider that manages the attack value. + */ + juce::Slider attackSlider; + + /** + * @brief The label that is linked to the attack slider. + */ + juce::Label attackLabel; + + /** + * @brief The slider that manages the release value. + */ + juce::Slider releaseSlider; + + /** + * @brief The label that is linked to the release slider. + */ + juce::Label releaseLabel; + + /** + * @brief The function that is called when the threshold value changes. + * @param value The new value of the threshold slider. + */ + void onThreholdValueChanged(NoiseGateEffect* effect, double value); + + /** + * @brief The function that is called when the release value changes. + * @param value The new value of the release slider. + */ + void onAttackValueChanged(NoiseGateEffect* effect, double value); + + /** + * @brief The function that is called when the release value changes. + * @param value The new value of the release slider. + */ + void onReleaseValueChanged(NoiseGateEffect* effect, double value); +}; \ No newline at end of file diff --git a/include/Pedal.h b/include/Pedal.h new file mode 100644 index 0000000..33af80c --- /dev/null +++ b/include/Pedal.h @@ -0,0 +1,5 @@ +#pragma once + +class Pedal { + +}; diff --git a/include/PedalPowerIndicatorComponent.h b/include/PedalPowerIndicatorComponent.h new file mode 100644 index 0000000..903508f --- /dev/null +++ b/include/PedalPowerIndicatorComponent.h @@ -0,0 +1,38 @@ +#pragma once + +#include + +/** + * @brief Represents a graphical component that indicates the power status of a pedal. + */ +class PedalPowerIndicatorComponent: public juce::Component +{ +public: + /** + * @brief Initializes a new instance of the PedalPowerIndicatorComponent class. + * @param isEnabled Indicates whether the pedal is enabled or not. + */ + explicit PedalPowerIndicatorComponent(bool isEnabled = true); + + /** + * @brief Destroys the instance of the PedalPowerIndicatorComponent class. + */ + ~PedalPowerIndicatorComponent() override; + + /** + * @brief Paints the component. + * @param g The JUCE graphics context that paints the component. + */ + void paint(juce::Graphics &g) override; + + /** + * @brief Toggles the power status of the pedal. + * @param isEnabled Indicates whether the pedal is enabled or not. + */ + void togglePower(bool isEnabled); +private: + /** + * @brief Indicates whether the pedal is enabled or not. + */ + bool isEnabled; +}; \ No newline at end of file diff --git a/include/PedalSettingsLayoutComponent.h b/include/PedalSettingsLayoutComponent.h new file mode 100644 index 0000000..655b6fa --- /dev/null +++ b/include/PedalSettingsLayoutComponent.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +class PedalSettingsLayoutComponent: public juce::Component +{ +public: + PedalSettingsLayoutComponent(juce::Grid* layout); + ~PedalSettingsLayoutComponent() override; + void paint(juce::Graphics& g) override; + void resized() override; +private: + juce::Grid* layout; +}; diff --git a/include/Pedalboard.h b/include/Pedalboard.h new file mode 100644 index 0000000..366ee95 --- /dev/null +++ b/include/Pedalboard.h @@ -0,0 +1,99 @@ +#pragma once + +#include "AbstractEffect.h" + +/** + * @brief Represents a pedalboard that contains all the effects and apply them on the audio stream. + * It inherits from the AbstractEffect class so we can include a pedalboard in another. + */ +class Pedalboard : public AbstractEffect { +public: + /** + * @brief Initializes a new instance of the Pedalboard class. + */ + Pedalboard(); + + /** + * @brief Initializes a new instance of the Pedalboard class with the given effects. + * @param effects The effects collection to initialize the pedalboard with. + */ + explicit Pedalboard(const std::vector &effects); + + /** + * @brief Destroys the instance of the Pedalboard class. + */ + ~Pedalboard() override; + + /** + * @brief Applies all the effects in the pedalboard to the given audio buffer. + * @param bufferToFill The audio buffer to apply the effects to. + */ + void apply(const AudioSourceChannelInfo &bufferToFill) override; + + /** + * @brief Compares the pedalboard with the given effect. + * @param effect The effect to compare with. + * @return True if the pedalboard is equal to the given effect, false otherwise. + */ + bool operator==(const AbstractEffect *effect) override; + + /** + * @brief Adds an effect at the end of the pedalboard. + * @param effect The effect to add to the pedalboard. + */ + void append(AbstractEffect *effect); + + /** + * @brief Adds all the effects from the given vector to the pedalboard. + * @param effects The vector of effects to add to the pedalboard. + */ + void appendAll(std::vector effects); + + /** + * @brief Inserts an effect at the given index in the pedalboard. + * @param effect The effect to insert into the pedalboard. + * @param index The index where to insert the effect. + */ + void insert(AbstractEffect *effect, int index); + + /** + * @brief Removes the given effect from the pedalboard. + * @param effect The effect to remove from the pedalboard. + */ + void remove(const AbstractEffect *effect); + + /** + * @brief Gets the collection of effects contained in the pedalboard. + * @return The collection of effects contained in the pedalboard. + */ + std::vector getEffects(); + + /** + * @brief Gets the type name of the effect for serialization purposes. + * @return A string representing the effect type. + */ + [[nodiscard]] String getEffectType() const override { return "DelayEffect"; } + + /** + * @brief Serializes the delay effect to a JSON object. + * @return JSON object containing serialized effect data. + */ + [[nodiscard]] var toJSON() const override { + auto obj = AbstractEffect::toJSON(); + return obj; + } + + /** + * @brief Deserializes the delay effect from a JSON object. + * @param json JSON object containing serialized effect data. + */ + void fromJSON(const var &json) override { + AbstractEffect::fromJSON(json); + } + +private: + /** + * @brief The collection of effects contained in the pedalboard. + */ + std::vector effects; +}; diff --git a/include/PedalboardComponent.h b/include/PedalboardComponent.h new file mode 100644 index 0000000..2b35414 --- /dev/null +++ b/include/PedalboardComponent.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include "Pedalboard.h" +#include "EffectComponent.h" + +/** + * @brief Represents a graphical component that contains and displays a pedalboard. + */ +class PedalboardComponent : public EffectComponent { + public: + /** + * @brief Initializes a new instance of the PedalboardComponent class. + * @param pedalboard The pedalboard to initialize the component with. + */ + PedalboardComponent(AbstractEffect* pedalboard); + + /** + * @brief Destroys the instance of the PedalboardComponent class. + */ + ~PedalboardComponent() override; + + /** + * @brief Adds an EffectComponent to the pedalboard. + * @param effect The EffectComponent to add to the pedalboard. + */ + void addEffect(EffectComponent* effect); + + /** + * @brief Determines what to do when the component is resized. + */ + void resized() override; + + /** + * @brief Determines how the component is displayed. + * @param g The JUCE graphics context that paints the component. + */ + void paint(juce::Graphics &g) override; + + /** + * @brief Gets the required width for the pedalboard based on its effects. + * @return The required width for the pedalboard. + */ + int getRequiredWidth() const; + + /** + * @brief Gets the required height for the pedalboard based on its effects. + * @return The required height for the pedalboard. + */ + int getRequiredHeight(const int boardWidth) const; + private: + + /** + * @brief The default margin between pedals. + */ + const int PEDALS_MARGIN = 20; + + /** + * @brief The collection of the effect components in the pedalboard. + */ + std::vector effectsComponents; + + /** + * @brief The flexbox that arranges layout for the effect components in the pedalboard. + */ + juce::FlexBox flexBox; +}; \ No newline at end of file diff --git a/include/PopupContentComponent.h b/include/PopupContentComponent.h new file mode 100644 index 0000000..38308a1 --- /dev/null +++ b/include/PopupContentComponent.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +class PopupContentComponent : public juce::Component +{ +public: + PopupContentComponent() + { + setSize(400, 300); + + addAndMakeVisible(closeButton); + closeButton.setButtonText("Fermer"); + closeButton.onClick = [this]() { + if (auto* dw = findParentComponentOfClass()) + dw->exitModalState(0); + }; + } + + void resized() override + { + closeButton.setBounds(getLocalBounds().reduced(20)); + } + +private: + juce::TextButton closeButton; +}; diff --git a/include/ResourceManager.h b/include/ResourceManager.h new file mode 100644 index 0000000..01d0670 --- /dev/null +++ b/include/ResourceManager.h @@ -0,0 +1,11 @@ +#pragma once +#include + +class ResourceManager +{ +public: + ResourceManager(); + ~ResourceManager(); + + static Image loadImage(const juce::String& relativePath); +}; diff --git a/include/SettingsComponent.h b/include/SettingsComponent.h new file mode 100644 index 0000000..8705784 --- /dev/null +++ b/include/SettingsComponent.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include +#include + +class SettingsComponent : public juce::Component { +public: + explicit SettingsComponent(juce::AudioDeviceManager& deviceManager); + + void resized() override; + +private: + juce::AudioDeviceSelectorComponent deviceSelector; +}; diff --git a/include/ToggleButtonComponent.h b/include/ToggleButtonComponent.h new file mode 100644 index 0000000..6b2e9b3 --- /dev/null +++ b/include/ToggleButtonComponent.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +class ToggleButtonComponent : public ToggleButton { +public: + ToggleButtonComponent(); + ~ToggleButtonComponent() override; + void paint(Graphics& g) override; +}; diff --git a/include/TopMenuBarComponent.h b/include/TopMenuBarComponent.h new file mode 100644 index 0000000..ad810ad --- /dev/null +++ b/include/TopMenuBarComponent.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include "AccountComponent.h" +#include "ModalOverlayComponent.h" +#include "SettingsComponent.h" + +class TopMenuBarComponent : public Component { +public: + explicit TopMenuBarComponent(AudioDeviceManager& deviceManager, + bool* isMuted = nullptr); + ~TopMenuBarComponent() override; + + void paint(Graphics& g) override; + void resized() override; + +private: + int buttonSize = 32; + float gap = 16; + + FlexBox flexBox; + + ImageButton accountButton; + ImageButton muteButton; + ImageButton settingsButton; + + AccountComponent* accountComponent; + ModalOverlayComponent* modalOverlay; + SettingsComponent* settingsComponent; + + bool* isSoundMuted = nullptr; + + void openSettingsPopup(AudioDeviceManager& deviceManager); + void openAccountPopup(); + void toggleMute(); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(TopMenuBarComponent) +}; diff --git a/resources/icons/account.png b/resources/icons/account.png new file mode 100644 index 0000000..fe445ac Binary files /dev/null and b/resources/icons/account.png differ diff --git a/resources/icons/export.png b/resources/icons/export.png new file mode 100644 index 0000000..69f11e2 Binary files /dev/null and b/resources/icons/export.png differ diff --git a/resources/icons/mute.png b/resources/icons/mute.png new file mode 100644 index 0000000..88dc4e0 Binary files /dev/null and b/resources/icons/mute.png differ diff --git a/resources/icons/power.png b/resources/icons/power.png new file mode 100644 index 0000000..dcfe593 Binary files /dev/null and b/resources/icons/power.png differ diff --git a/resources/icons/settings.png b/resources/icons/settings.png new file mode 100644 index 0000000..2071f27 Binary files /dev/null and b/resources/icons/settings.png differ diff --git a/resources/icons/sync.png b/resources/icons/sync.png new file mode 100644 index 0000000..cba8294 Binary files /dev/null and b/resources/icons/sync.png differ diff --git a/resources/icons/unmute.png b/resources/icons/unmute.png new file mode 100644 index 0000000..d1e4d79 Binary files /dev/null and b/resources/icons/unmute.png differ diff --git a/resources/icons/xmark.png b/resources/icons/xmark.png new file mode 100644 index 0000000..4937ee7 Binary files /dev/null and b/resources/icons/xmark.png differ diff --git a/resources/images/background.png b/resources/images/background.png new file mode 100644 index 0000000..07a4d50 Binary files /dev/null and b/resources/images/background.png differ diff --git a/resources/images/kamp_logo_with_background.png b/resources/images/kamp_logo_with_background.png new file mode 100644 index 0000000..0ca379d Binary files /dev/null and b/resources/images/kamp_logo_with_background.png differ diff --git a/src/ApiClient.cpp b/src/ApiClient.cpp new file mode 100644 index 0000000..a057c75 --- /dev/null +++ b/src/ApiClient.cpp @@ -0,0 +1,22 @@ +#include +#include "ApiClient.h" + +using ReplyFunc = std::function; + +void ApiClient::runHTTP(const URL &url, const ReplyFunc &replyFunc) { + auto req = [url, replyFunc] { + const auto options = URL::InputStreamOptions(URL::ParameterHandling::inAddress); + if (const auto stream = url.createInputStream(options); stream != nullptr) { + auto content = stream->readString(); + MessageManager::callAsync([replyFunc, content] { + replyFunc(content); + }); + } else { + MessageManager::callAsync([replyFunc] { + replyFunc("Error connecting to server"); + }); + } + }; + + Thread::launch(req); +} diff --git a/src/AudioBuffer.cpp b/src/AudioBuffer.cpp new file mode 100644 index 0000000..291454e --- /dev/null +++ b/src/AudioBuffer.cpp @@ -0,0 +1,9 @@ +#include "AudioBuffer.h" + +AudioBuffer::AudioBuffer() {} + +AudioBuffer::~AudioBuffer() {} + +juce::AudioSourceChannelInfo AudioBuffer::getBuffer() { + return bufferToFill; +} \ No newline at end of file diff --git a/src/EffectComponentFactory.cpp b/src/EffectComponentFactory.cpp new file mode 100644 index 0000000..a9a9df0 --- /dev/null +++ b/src/EffectComponentFactory.cpp @@ -0,0 +1,32 @@ +#include "AbstractEffect.h" +#include "DelayEffectComponent.h" +#include "DistortionEffect.h" +#include "DistortionEffectComponent.h" +#include "EffectComponentFactory.h" +#include "EqualizerEffect.h" +#include "EqualizerEffectComponent.h" +#include "NoiseGateEffect.h" +#include "NoiseGateEffectComponent.h" +#include "ChorusEffect.h" +#include "ChorusEffectComponent.h" + +EffectComponent* EffectComponentFactory::CreateEffectComponent(AbstractEffect* effect) { + if (dynamic_cast(effect) != nullptr) { + return new DelayEffectComponent(effect); + } + if (dynamic_cast(effect) != nullptr) { + return new DistortionEffectComponent(effect); + } + if (dynamic_cast(effect) != nullptr) + { + return new EqualizerEffectComponent(effect); + } + if (dynamic_cast(effect) != nullptr) { + return new NoiseGateEffectComponent(effect); + } + if (dynamic_cast(effect) != nullptr) + { + return new ChorusEffectComponent(effect); + } + return nullptr; +} diff --git a/src/Main.cpp b/src/Main.cpp index 22bbaad..895ebdb 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -1,101 +1,81 @@ +#include +#include "EffectsFactory.h" #include "MainComponent.h" -//============================================================================== -class GuiAppApplication final : public juce::JUCEApplication -{ +class GuiAppApplication final : public JUCEApplication { public: - //============================================================================== - GuiAppApplication() {} - - // We inject these as compile definitions from the CMakeLists.txt - // If you've enabled the juce header with `juce_generate_juce_header()` - // you could `#include ` and use `ProjectInfo::projectName` etc. instead. - const juce::String getApplicationName() override { return JUCE_APPLICATION_NAME_STRING; } - const juce::String getApplicationVersion() override { return JUCE_APPLICATION_VERSION_STRING; } - bool moreThanOneInstanceAllowed() override { return true; } - - //============================================================================== - void initialise (const juce::String& commandLine) override - { - // This method is where you should put your application's initialisation code.. - juce::ignoreUnused (commandLine); - - mainWindow.reset (new MainWindow (getApplicationName())); - } - - void shutdown() override - { - // Add your application's shutdown code here.. - - mainWindow = nullptr; // (deletes our window) - } - - //============================================================================== - void systemRequestedQuit() override - { - // This is called when the app is being asked to quit: you can ignore this - // request and let the app carry on running, or call quit() to allow the app to close. - quit(); - } - - void anotherInstanceStarted (const juce::String& commandLine) override - { - // When another instance of the app is launched while this one is running, - // this method is invoked, and the commandLine parameter tells you what - // the other instance's command-line arguments were. - juce::ignoreUnused (commandLine); - } - - //============================================================================== - /* - This class implements the desktop window that contains an instance of - our MainComponent class. - */ - class MainWindow final : public juce::DocumentWindow - { - public: - explicit MainWindow (juce::String name) - : DocumentWindow (name, - juce::Desktop::getInstance().getDefaultLookAndFeel() - .findColour (backgroundColourId), - allButtons) - { - setUsingNativeTitleBar (true); - setContentOwned (new MainComponent(), true); - - #if JUCE_IOS || JUCE_ANDROID + GuiAppApplication() = default; + + const String getApplicationName() override { + return JUCE_APPLICATION_NAME_STRING; + } + + const String getApplicationVersion() override { + return JUCE_APPLICATION_VERSION_STRING; + } + + bool moreThanOneInstanceAllowed() override { return false; } + + void initialise(const String& commandLine) override { + // This method is where you should put your application's initialisation code.. + ignoreUnused(commandLine); + + mainWindow = std::make_unique(getApplicationName()); + } + + void shutdown() override { + mainWindow = nullptr; // (deletes our window) + } + + void systemRequestedQuit() override { + // This is called when the app is being asked to quit: you can ignore this + // request and let the app carry on running, or call quit() to allow the app to close. + quit(); + } + + void anotherInstanceStarted(const String& commandLine) override { + // When another instance of the app is launched while this one is running, + // this method is invoked, and the commandLine parameter tells you what + // the other instance's command-line arguments were. + ignoreUnused(commandLine); + } + + class MainWindow final : public DocumentWindow { + public: + explicit MainWindow(const String& name) : + DocumentWindow( + name, Desktop::getInstance().getDefaultLookAndFeel().findColour( + backgroundColourId), allButtons) { + setUsingNativeTitleBar(true); + Pedalboard* pedalboard = new Pedalboard(); + pedalboard->appendAll(EffectsFactory::createAllEffects()); + Manager* manager = new Manager(pedalboard); + setContentOwned(new MainComponent(*manager), true); + +#if JUCE_IOS || JUCE_ANDROID setFullScreen (true); - #else - setResizable (true, true); - centreWithSize (getWidth(), getHeight()); - #endif - - setVisible (true); - } - - void closeButtonPressed() override - { - // This is called when the user tries to close this window. Here, we'll just - // ask the app to quit when this happens, but you can change this to do - // whatever you need. - getInstance()->systemRequestedQuit(); - } - - /* Note: Be careful if you override any DocumentWindow methods - the base - class uses a lot of them, so by overriding you might break its functionality. - It's best to do all your work in your content component instead, but if - you really have to override any DocumentWindow methods, make sure your - subclass also calls the superclass's method. - */ - - private: - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainWindow) - }; +#else + setResizable(true, true); + setSize(1280, 854); + centreWithSize(1280, 854); +#endif + Component::setVisible(true); + } + + void closeButtonPressed() override { + // This is called when the user tries to close this window. Here, we'll just + // ask the app to quit when this happens, but you can change this to do + // whatever you need. + getInstance()->systemRequestedQuit(); + } + + private: + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainWindow) + }; private: - std::unique_ptr mainWindow; + std::unique_ptr mainWindow; }; -//============================================================================== // This macro generates the main() routine that launches the app. -START_JUCE_APPLICATION (GuiAppApplication) +START_JUCE_APPLICATION(GuiAppApplication) diff --git a/src/MainComponent.cpp b/src/MainComponent.cpp index c7dcd2f..275967a 100644 --- a/src/MainComponent.cpp +++ b/src/MainComponent.cpp @@ -1,25 +1,71 @@ #include "MainComponent.h" +#include "ResourceManager.h" -//============================================================================== -MainComponent::MainComponent() -{ - setSize (900, 700); +MainComponent::MainComponent(const Manager& manager): + pedalboardComponent(manager.getPedalboard()), + topMenuBarComponent(this->deviceManager, &isSoundMuted), manager(manager) { + setAudioChannels(2, 2); + + const Image background = ResourceManager::loadImage( + "resources/images/background.png"); + if (background.isValid()) { + backgroundImage.setImage(background); + backgroundImage.setImagePlacement(RectanglePlacement::stretchToFit); + addAndMakeVisible(backgroundImage); + } else { + DBG("Error : resources/images/background.png not found."); + } + + pedalboardContainer.setViewedComponent(&pedalboardComponent, true); + pedalboardContainer.setScrollBarsShown(true, false); + + addAndMakeVisible(pedalboardContainer); + addAndMakeVisible(topMenuBarComponent); + addAndMakeVisible(bottomMenuBarComponent); + addAndMakeVisible(connectionComponent); } //============================================================================== -void MainComponent::paint (juce::Graphics& g) -{ - // (Our component is opaque, so we must completely fill the background with a solid colour) - g.fillAll ( juce::Colours::black); - g.setFont (juce::FontOptions (16.0f)); - g.setColour (juce::Colours::white); - g.drawText ("Hello kAmp!", getLocalBounds(), juce::Justification::centred, true); +void MainComponent::paint(Graphics& g) {} + +void MainComponent::resized() { + backgroundImage.setBounds(getLocalBounds()); + connectionComponent.setBounds(getLocalBounds()); + int pedalboardWidth = getWidth(); + int pedalboardHeight = pedalboardComponent.getRequiredHeight(getWidth()); + pedalboardComponent.setSize(pedalboardWidth, pedalboardHeight); + + using Track = Grid::TrackInfo; + using Px = Grid::Px; + using Fr = Grid::Fr; + grid.templateRows = {Track(Px(50)), Track(Fr(1))}; + grid.templateColumns = {Track(Fr(1))}; + grid.items = { + GridItem(topMenuBarComponent), + GridItem(pedalboardContainer) + }; + grid.performLayout(getLocalBounds()); } -void MainComponent::resized() -{ - // This is called when the MainComponent is resized. - // If you add any child components, this is where you should - // update their positions. +void MainComponent::prepareToPlay(int samplesPerBlockExpected, + double sampleRate) {} + +void MainComponent::getNextAudioBlock( + const AudioSourceChannelInfo& bufferToFill) { + if (!this->isSoundMuted) { + if (&bufferToFill == nullptr) { + return; + } + if (bufferToFill.buffer == nullptr) { + return; + } + this->manager.apply(bufferToFill); + } else { + bufferToFill.clearActiveBufferRegion(); + } } + +void MainComponent::releaseResources() {} + + diff --git a/src/Manager.cpp b/src/Manager.cpp new file mode 100644 index 0000000..860ba67 --- /dev/null +++ b/src/Manager.cpp @@ -0,0 +1,30 @@ +#include "Manager.h" + +Manager::Manager(AbstractEffect *pedalboard) : pedalboard(pedalboard) { +} + +void Manager::apply(const AudioSourceChannelInfo &bufferToFill) const { + if (bufferToFill.buffer == nullptr) { + return; + } + pedalboard->apply(bufferToFill); +} + +void Manager::importF() const { +} + +void Manager::append(AbstractEffect* effect) const { + if (Pedalboard* pedalboard = dynamic_cast(this->pedalboard)) { + pedalboard->append(effect); + } +} + +void Manager::exportAll() const { +} + +void Manager::exportSelection() const { +} + +AbstractEffect *Manager::getPedalboard() const { + return pedalboard; +} diff --git a/src/Pedalboard.cpp b/src/Pedalboard.cpp new file mode 100644 index 0000000..076b7e9 --- /dev/null +++ b/src/Pedalboard.cpp @@ -0,0 +1,46 @@ +#include "DelayEffect.h" +#include "DistortionEffect.h" +#include "Pedalboard.h" +#include "EqualizerEffect.h" +#include "NoiseGateEffect.h" + +Pedalboard::Pedalboard() { }; + +Pedalboard::~Pedalboard() = default; + +void Pedalboard::apply(const AudioSourceChannelInfo &bufferToFill) { + for (AbstractEffect* effect : effects) { + if (*(effect->isEnabled)) + { + effect->apply(bufferToFill); + } + } +} + +void Pedalboard::append(AbstractEffect* effect) { + effects.push_back(effect); +} + +void Pedalboard::appendAll(std::vector effects) { + this->effects.insert(this->effects.end(), effects.begin(), effects.end()); +} + +void Pedalboard::insert(AbstractEffect* effect, int index) { + effects.insert(effects.begin() + index, effect); +} + +void Pedalboard::remove(const AbstractEffect* effect) { + effects.erase(std::remove(effects.begin(), effects.end(), effect), effects.end()); +} + +std::vector Pedalboard::getEffects() { + return effects; +} + +Pedalboard::Pedalboard(const std::vector &effects) { + this->effects = effects; +} + +bool Pedalboard::operator==(const AbstractEffect* effect) { + return true; +} \ No newline at end of file diff --git a/src/components/AccountComponent.cpp b/src/components/AccountComponent.cpp new file mode 100644 index 0000000..09662e2 --- /dev/null +++ b/src/components/AccountComponent.cpp @@ -0,0 +1,104 @@ +#include "AccountComponent.h" +#include "ApiClient.h" + +namespace { +constexpr float titleFontSize = 24.0f; +constexpr float labelFontSize = 16.0f; +constexpr float responseFontSize = 14.0f; +} + +AccountComponent::AccountComponent() { + setupLabels(); + setupButtons(); + setupGrid(); + + // Dummy API call + auto replyFunc = [this](const String& content) { + apiResponseReceived(content); + }; + ApiClient::runHTTP({"https://dummyjson.com/test"}, replyFunc); +} + +void AccountComponent::setupLabels() { + titleLabel.setFont(FontOptions(titleFontSize, Font::bold)); + titleLabel.setJustificationType(Justification::centred); + addAndMakeVisible(titleLabel); + + for (auto* label : {&emailLabel, &usernameLabel}) { + label->setFont(FontOptions(labelFontSize)); + label->setJustificationType(Justification::centredLeft); + addAndMakeVisible(*label); + } + + for (auto* val : {&emailValueLabel, &usernameValueLabel}) { + val->setFont(FontOptions(labelFontSize)); + val->setJustificationType(Justification::centredLeft); + val->setColour(Label::textColourId, Colours::black); + addAndMakeVisible(*val); + } + + responseLabel.setFont(FontOptions(responseFontSize)); + responseLabel.setJustificationType(Justification::centred); + responseLabel.setColour(Label::textColourId, Colours::darkgrey); + addAndMakeVisible(responseLabel); +} + +void AccountComponent::setupButtons() { + saveButton.onClick = [this] { saveSettings(); }; + importButton.onClick = [this] { importSettings(); }; + + for (auto* button : {&saveButton, &importButton}) + addAndMakeVisible(*button); +} + +void AccountComponent::setupGrid() { + using namespace juce; + grid.templateRows = { + Grid::TrackInfo(40_px), // Title + Grid::TrackInfo(30_px), // Email label + Grid::TrackInfo(30_px), // Email value + Grid::TrackInfo(30_px), // Username label + Grid::TrackInfo(30_px), // Username value + Grid::TrackInfo(20_px), // Spacer + Grid::TrackInfo(40_px), // Change password button + Grid::TrackInfo(40_px), // Save button + Grid::TrackInfo(40_px), // Import button + Grid::TrackInfo(30_px) // Response label + }; + + grid.templateColumns = {Grid::TrackInfo(1_fr)}; + + grid.items = { + GridItem(titleLabel), + GridItem(emailLabel), + GridItem(emailValueLabel), + GridItem(usernameLabel), + GridItem(usernameValueLabel), + GridItem().withArea(6, 1), // Spacer + GridItem(saveButton), + GridItem(importButton), + GridItem(responseLabel) + }; +} + +void AccountComponent::paint(Graphics& g) { + g.fillAll(Colours::lightgrey); +} + +void AccountComponent::resized() { + grid.performLayout(getLocalBounds().reduced(40)); +} + +void AccountComponent::apiResponseReceived(const String& content) { + responseLabel.setText("Réponse API : " + content, dontSendNotification); +} + +void AccountComponent::saveSettings() { + responseLabel.setText("✅ Réglages sauvegardés !", dontSendNotification); +} + +void AccountComponent::importSettings() { + emailValueLabel.setText("import@example.com", dontSendNotification); + usernameValueLabel.setText("UtilisateurImporté", dontSendNotification); + responseLabel.setText("📥 Réglages importés", dontSendNotification); +} diff --git a/src/components/BottomMenuBarComponent.cpp b/src/components/BottomMenuBarComponent.cpp new file mode 100644 index 0000000..ec40f5a --- /dev/null +++ b/src/components/BottomMenuBarComponent.cpp @@ -0,0 +1,15 @@ +#include "BottomMenuBarComponent.h" + +BottomMenuBarComponent::BottomMenuBarComponent() { + addAndMakeVisible(menuBarComponent); +} + +BottomMenuBarComponent::~BottomMenuBarComponent() = default; + +void BottomMenuBarComponent::paint(juce::Graphics &g) { + g.fillAll(juce::Colours::white); +} + +void BottomMenuBarComponent::resized() { + menuBarComponent.setBounds(getLocalBounds()); +} \ No newline at end of file diff --git a/src/components/ChorusEffectComponent.cpp b/src/components/ChorusEffectComponent.cpp new file mode 100644 index 0000000..fad9ba8 --- /dev/null +++ b/src/components/ChorusEffectComponent.cpp @@ -0,0 +1,98 @@ +#include "ChorusEffectComponent.h" +#include + +ChorusEffectComponent::ChorusEffectComponent(AbstractEffect* e) + : BasePedalComponent(e), chorusEffect(dynamic_cast(e)) +{ + jassert(chorusEffect != nullptr); + + primaryColor = juce::Colours::cornflowerblue; + + using Track = juce::Grid::TrackInfo; + using Fr = juce::Grid::Fr; + + grid.templateRows = { Track(Fr(1)), Track(Fr(2)) }; + grid.templateColumns = { Track(Fr(1)), Track(Fr(1)), Track(Fr(1)) }; + + // ----- Sliders ----- + // Depth + depthSlider.setSliderStyle(juce::Slider::Rotary); + depthSlider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 50, 20); + depthSlider.setRange(0.0, 1.0, 0.01); + depthSlider.setValue(chorusEffect->getDepth()); + depthSlider.onValueChange = [this]() { + chorusEffect->setDepth(depthSlider.getValue()); + }; + depthLabel.setText("Depth", juce::dontSendNotification); + depthLabel.setJustificationType(juce::Justification::centred); + + // Rate + rateSlider.setSliderStyle(juce::Slider::Rotary); + rateSlider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 50, 20); + rateSlider.setRange(0.0, 10.0, 0.01); + rateSlider.setValue(chorusEffect->getRate()); + rateSlider.onValueChange = [this]() { + chorusEffect->setRate(rateSlider.getValue()); + }; + rateLabel.setText("Rate", juce::dontSendNotification); + rateLabel.setJustificationType(juce::Justification::centred); + + // Mix + mixSlider.setSliderStyle(juce::Slider::Rotary); + mixSlider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 50, 20); + mixSlider.setRange(0.0, 1.0, 0.01); + mixSlider.setValue(chorusEffect->getMix()); + mixSlider.onValueChange = [this]() { + chorusEffect->setMix(mixSlider.getValue()); + }; + mixLabel.setText("Mix", juce::dontSendNotification); + mixLabel.setJustificationType(juce::Justification::centred); + + // ----- Ajouter au Grid ----- + grid.items.addArray({ + juce::GridItem(depthLabel), juce::GridItem(rateLabel), juce::GridItem(mixLabel), + juce::GridItem(depthSlider), juce::GridItem(rateSlider), juce::GridItem(mixSlider), + }); + + settingsLayout = new PedalSettingsLayoutComponent(&grid); + + addAndMakeVisible(settingsLayout); // <<== Ajout crucial pour afficher le layout + + if (chorusEffect != nullptr) + { + double defaultSampleRate = 44100.0; + int defaultBlockSize = 512; + int defaultNumChannels = 2; + + chorusEffect->prepare(defaultSampleRate, defaultBlockSize, defaultNumChannels); + } + + // Ajout visible + addAndMakeVisible(depthSlider); + addAndMakeVisible(depthLabel); + addAndMakeVisible(rateSlider); + addAndMakeVisible(rateLabel); + addAndMakeVisible(mixSlider); + addAndMakeVisible(mixLabel); + + // Positionnement manuel (à ajuster en fonction de ta taille) + auto area = getLocalBounds(); + int sliderWidth = area.getWidth() / 3; + int sliderHeight = area.getHeight() / 2; + + depthSlider.setBounds(0, sliderHeight, sliderWidth, sliderHeight); + depthLabel.setBounds(0, 0, sliderWidth, sliderHeight / 2); + + rateSlider.setBounds(sliderWidth, sliderHeight, sliderWidth, sliderHeight); + rateLabel.setBounds(sliderWidth, 0, sliderWidth, sliderHeight / 2); + + mixSlider.setBounds(sliderWidth * 2, sliderHeight, sliderWidth, sliderHeight); + mixLabel.setBounds(sliderWidth * 2, 0, sliderWidth, sliderHeight / 2); + + + this->initializePedal(); + setSize(300, 300); +} + + + diff --git a/src/components/EffectComponent.cpp b/src/components/EffectComponent.cpp new file mode 100644 index 0000000..fbcaa23 --- /dev/null +++ b/src/components/EffectComponent.cpp @@ -0,0 +1,10 @@ +#include "EffectComponent.h" +#include "AbstractEffect.h" + +EffectComponent::EffectComponent(AbstractEffect* effect) : effect(effect) { + this->effect = effect; +} + +AbstractEffect* EffectComponent::getEffect() { + return effect; +} \ No newline at end of file diff --git a/src/components/LoginComponent.cpp b/src/components/LoginComponent.cpp new file mode 100644 index 0000000..77dbaaa --- /dev/null +++ b/src/components/LoginComponent.cpp @@ -0,0 +1,49 @@ +#include "LoginComponent.h" + +LoginComponent::LoginComponent() { + setupFields(); + setupButtons(); + layoutComponents(); +} + +LoginComponent::~LoginComponent() = default; + +void LoginComponent::setupFields() { + usernameField.setTextToShowWhenEmpty("Username", Colours::grey); + passwordField.setTextToShowWhenEmpty("Password", Colours::grey); + passwordField.setPasswordCharacter('*'); + addAndMakeVisible(usernameField); + addAndMakeVisible(passwordField); +} + +void LoginComponent::setupButtons() { + loginButton.onClick = [] { + // TODO: Implement login logic + }; + skipButton.onClick = [this] { skipButtonClicked(); }; + addAndMakeVisible(loginButton); + addAndMakeVisible(skipButton); +} + +void LoginComponent::paint(Graphics& g) { + g.fillAll(Colours::white); +} + +void LoginComponent::resized() { + layoutComponents(); +} + +void LoginComponent::layoutComponents() { + auto area = getLocalBounds().reduced(50); + auto halfWidth = area.getWidth() / 2; + auto halfHeight = area.getHeight() / 2; + + usernameField.setBounds(halfWidth - 100, halfHeight - 80, 300, 40); + passwordField.setBounds(halfWidth - 100, halfHeight - 30, 300, 40); + loginButton.setBounds(halfWidth - 100, halfHeight + 30, 300, 40); + skipButton.setBounds(halfWidth - 100, halfHeight + 80, 300, 40); +} + +void LoginComponent::skipButtonClicked() { + setVisible(false); +} \ No newline at end of file diff --git a/src/components/PedalPowerIndicatorComponent.cpp b/src/components/PedalPowerIndicatorComponent.cpp new file mode 100644 index 0000000..2d3724a --- /dev/null +++ b/src/components/PedalPowerIndicatorComponent.cpp @@ -0,0 +1,20 @@ +#include "PedalPowerIndicatorComponent.h" + +PedalPowerIndicatorComponent::PedalPowerIndicatorComponent(bool isEnabled) +{ + this->isEnabled = isEnabled; +} + +PedalPowerIndicatorComponent::~PedalPowerIndicatorComponent() = default; + +void PedalPowerIndicatorComponent::paint(juce::Graphics &g) +{ + g.setColour(isEnabled ? juce::Colours::green : juce::Colours::red); + g.fillEllipse(juce::Rectangle((getWidth() / 2) - 10,(getHeight() / 2) - 10,20,20)); +} + +void PedalPowerIndicatorComponent::togglePower(bool isEnabled) +{ + this->isEnabled = isEnabled; + repaint(); +} \ No newline at end of file diff --git a/src/components/PedalSettingsLayoutComponent.cpp b/src/components/PedalSettingsLayoutComponent.cpp new file mode 100644 index 0000000..684216d --- /dev/null +++ b/src/components/PedalSettingsLayoutComponent.cpp @@ -0,0 +1,13 @@ +#include "PedalSettingsLayoutComponent.h" + +PedalSettingsLayoutComponent::PedalSettingsLayoutComponent(juce::Grid* layout) + : layout(layout) {} + +PedalSettingsLayoutComponent::~PedalSettingsLayoutComponent() = default; + +void PedalSettingsLayoutComponent::paint(juce::Graphics& g) { +} + +void PedalSettingsLayoutComponent::resized() { + layout->performLayout(getLocalBounds()); +} \ No newline at end of file diff --git a/src/components/PedalboardComponent.cpp b/src/components/PedalboardComponent.cpp new file mode 100644 index 0000000..6804424 --- /dev/null +++ b/src/components/PedalboardComponent.cpp @@ -0,0 +1,68 @@ +#include "PedalboardComponent.h" +#include "EffectComponentFactory.h" + +PedalboardComponent::~PedalboardComponent() = default; + +PedalboardComponent::PedalboardComponent(AbstractEffect* pedalboard) : EffectComponent(pedalboard) { + if (Pedalboard* pedalboard = dynamic_cast(this->effect)) { + for (AbstractEffect *effect : pedalboard->getEffects()) { + addEffect(EffectComponentFactory::CreateEffectComponent(effect)); + } + } + flexBox.flexDirection = juce::FlexBox::Direction::row; + flexBox.justifyContent = juce::FlexBox::JustifyContent::center; + flexBox.alignItems = juce::FlexBox::AlignItems::center; + flexBox.flexWrap = juce::FlexBox::Wrap::wrap; + for (auto &effectComponent : effectsComponents) { + flexBox.items.add(juce::FlexItem(*effectComponent).withWidth(effectComponent->getWidth()).withHeight(effectComponent->getHeight()).withMargin(PEDALS_MARGIN)); + } +} + +void PedalboardComponent::resized() { + flexBox.performLayout(getLocalBounds()); +} + +void PedalboardComponent::paint(juce::Graphics &g) { +} + +void PedalboardComponent::addEffect(EffectComponent* effect) { + effectsComponents.push_back(effect); + addAndMakeVisible(effect); +} + + +int PedalboardComponent::getRequiredWidth() const { + int totalWidth = 0; + for (const auto& effectComponent : effectsComponents) { + totalWidth += effectComponent->getWidth() + PEDALS_MARGIN * 2; + } + return totalWidth; +} + +int PedalboardComponent::getRequiredHeight(const int boardWidth) const { + if (effectsComponents.empty()) + return 0; + int x = 0; + int maxHeightInRow = 0; + int totalHeight = 0; + const int margin = PEDALS_MARGIN; + + for (const auto* effectComponent : effectsComponents) { + int effectWidth = effectComponent->getWidth() + margin * 2; + int effectHeight = effectComponent->getHeight() + margin * 2; + + if (x + effectWidth > boardWidth && x > 0) { + totalHeight += maxHeightInRow; + x = 0; + maxHeightInRow = 0; + } + + x += effectWidth; + if (effectHeight > maxHeightInRow) + maxHeightInRow = effectHeight; + } + + totalHeight += maxHeightInRow; + + return totalHeight; +} \ No newline at end of file diff --git a/src/components/SettingsComponent.cpp b/src/components/SettingsComponent.cpp new file mode 100644 index 0000000..6c3e3fd --- /dev/null +++ b/src/components/SettingsComponent.cpp @@ -0,0 +1,12 @@ +#include "SettingsComponent.h" + +SettingsComponent::SettingsComponent(juce::AudioDeviceManager& deviceManager) + : deviceSelector(deviceManager, 0, 2, 0, 2, true, true, false, false) +{ + addAndMakeVisible(deviceSelector); +} + +void SettingsComponent::resized() +{ + deviceSelector.setBounds(10, 50, getWidth() - 20, getHeight() - 60); +} diff --git a/src/components/TopMenuBarComponent.cpp b/src/components/TopMenuBarComponent.cpp new file mode 100644 index 0000000..322d024 --- /dev/null +++ b/src/components/TopMenuBarComponent.cpp @@ -0,0 +1,148 @@ +#include "AccountComponent.h" +#include "ModalOverlayComponent.h" +#include "PopupContentComponent.h" +#include "SettingsComponent.h" +#include "TopMenuBarComponent.h" +#include "ResourceManager.h" + +TopMenuBarComponent::TopMenuBarComponent(AudioDeviceManager& deviceManager, + bool* isMuted) { + this->isSoundMuted = isMuted; + + Image settingsImage = ResourceManager::loadImage( + "resources/icons/settings.png"); + if (settingsImage.isValid()) { + settingsButton.setImages(true, true, true, settingsImage, 1.0f, {}, + settingsImage, 1.0f, {}, settingsImage, 1.0f, + {}); + settingsButton.setSize(settingsImage.getWidth(), + settingsImage.getHeight()); + addAndMakeVisible(settingsButton); + } else { + DBG("Erreur : image settings.png introuvable ou invalide."); + } + + Image accountImage = ResourceManager::loadImage( + "resources/icons/account.png"); + if (accountImage.isValid()) { + accountButton.setImages(true, true, true, accountImage, 1.0f, {}, + accountImage, 1.0f, {}, accountImage, 1.0f, {}); + accountButton.setSize(accountImage.getWidth(), + accountImage.getHeight()); + addAndMakeVisible(accountButton); + } else { + DBG("Erreur : image account.png introuvable ou invalide."); + } + + Image muteImage = ResourceManager::loadImage("resources/icons/unmute.png"); + if (muteImage.isValid()) { + muteButton.setImages(true, true, true, muteImage, 1.0f, {}, muteImage, + 1.0f, {}, muteImage, 1.0f, {}); + muteButton.setSize(muteImage.getWidth(), muteImage.getHeight()); + addAndMakeVisible(muteButton); + } else { + DBG("Erreur : image mute.png introuvable ou invalide."); + } + +#if !JUCE_IOS + settingsButton.onClick = [this, &deviceManager] { + openSettingsPopup(deviceManager); + }; +#endif + accountButton.onClick = [this] { openAccountPopup(); }; + muteButton.onClick = [this] { toggleMute(); }; + + flexBox.justifyContent = FlexBox::JustifyContent::flexEnd; + flexBox.alignItems = FlexBox::AlignItems::center; + flexBox.items.add( + FlexItem(muteButton).withWidth(buttonSize).withHeight(buttonSize). + withMargin({0, gap, 0, 0})); + flexBox.items.add( + FlexItem(settingsButton).withWidth(buttonSize).withHeight(buttonSize). + withMargin({0, gap, 0, 0})); + flexBox.items.add( + FlexItem(accountButton).withWidth(buttonSize).withHeight(buttonSize). + withMargin({0, gap, 0, 0})); +} + +TopMenuBarComponent::~TopMenuBarComponent() = default; + +void TopMenuBarComponent::paint(Graphics& g) { + const ColourGradient gradient(Colours::black, 0, 0, + Colours::transparentBlack, 0, + static_cast(getHeight()), false); + g.setGradientFill(gradient); + g.fillAll(); + + const FontOptions font("Times New Roman", 24.0f, Font::bold | Font::italic); + g.setFont(font); + g.setColour(Colours::white); + const int topMargin = (getHeight() - 24) / 2; + + g.drawText("kAmp", gap, topMargin, 80, 24, Justification::left); +} + + +void TopMenuBarComponent::resized() { + auto* mainWindow = getTopLevelComponent(); + if (mainWindow == nullptr) + return; + if (modalOverlay != nullptr) { + modalOverlay->setBounds(mainWindow->getLocalBounds()); + } + if (settingsComponent != nullptr) { + settingsComponent->setBounds(mainWindow->getLocalBounds()); + } + if (accountComponent != nullptr) { + accountComponent->setBounds(mainWindow->getLocalBounds()); + } + flexBox.performLayout(getLocalBounds()); +} + +void TopMenuBarComponent::openSettingsPopup(AudioDeviceManager& deviceManager) { + settingsComponent = new SettingsComponent(deviceManager); + auto* mainWindow = getTopLevelComponent(); + if (mainWindow == nullptr) + return; + + modalOverlay = new ModalOverlayComponent("Audio settings", + settingsComponent); + mainWindow->addAndMakeVisible(modalOverlay); + modalOverlay->setBounds(mainWindow->getLocalBounds()); +} + +void TopMenuBarComponent::openAccountPopup() { + accountComponent = new AccountComponent(); + auto* mainWindow = getTopLevelComponent(); + if (mainWindow == nullptr) + return; + + modalOverlay = new ModalOverlayComponent("Account", accountComponent); + mainWindow->addAndMakeVisible(modalOverlay); + modalOverlay->setBounds(mainWindow->getLocalBounds()); +} + +void TopMenuBarComponent::toggleMute() { + if (*this->isSoundMuted) { + Image muteImage = ResourceManager::loadImage( + "resources/icons/unmute.png"); + if (muteImage.isValid()) { + muteButton.setImages(false, true, true, muteImage, 1.0f, {}, + muteImage, 1.0f, {}, muteImage, 1.0f, {}); + addAndMakeVisible(muteButton); + } else { + DBG("Erreur : image mute.png introuvable ou invalide."); + } + } else { + Image muteImage = + ResourceManager::loadImage("resources/icons/mute.png"); + if (muteImage.isValid()) { + muteButton.setImages(false, true, true, muteImage, 1.0f, {}, + muteImage, 1.0f, {}, muteImage, 1.0f, {}); + addAndMakeVisible(muteButton); + } else { + DBG("Erreur : image mute.png introuvable ou invalide."); + } + } + *(this->isSoundMuted) = !(*(this->isSoundMuted)); +} diff --git a/src/components/effects/BasePedalComponent.cpp b/src/components/effects/BasePedalComponent.cpp new file mode 100644 index 0000000..9409b25 --- /dev/null +++ b/src/components/effects/BasePedalComponent.cpp @@ -0,0 +1,68 @@ +#include "BasePedalComponent.h" +#include "ResourceManager.h" + +BasePedalComponent::BasePedalComponent(AbstractEffect* effect) : + EffectComponent(effect) { + isEnabled = effect->isEnabled; +} + +BasePedalComponent::~BasePedalComponent() = default; + +void BasePedalComponent::paint(Graphics& g) { + g.setColour(primaryColor); + g.fillRoundedRectangle(0, 0, getWidth(), getHeight(), 15); +} + +void BasePedalComponent::resized() { + pedalLayout.performLayout(getLocalBounds()); +} + +void BasePedalComponent::onEnableButtonClicked() { + *isEnabled = !(*isEnabled); + enablePedalButton.setToggleState(*isEnabled, dontSendNotification); + isEnabledIndicator->togglePower(*isEnabled); +} + +void BasePedalComponent::initializePedal() { + isEnabledIndicator = new PedalPowerIndicatorComponent(*isEnabled); + pedalLabel = new Label(); + pedalLabel->setText(getEffect()->effectName, dontSendNotification); + pedalLabel->setJustificationType(Justification::centred); + pedalLabel->setFont(FontOptions(30.0f, Font::bold)); + + Image powerImage = ResourceManager::loadImage("resources/icons/power.png"); + if (powerImage.isValid()) { + enablePedalButton.setImages(true, true, true, + powerImage, 1.0f, {}, + powerImage, 1.0f, {}, + powerImage, 1.0f, {}); + enablePedalButton.onClick = [this] { + this->onEnableButtonClicked(); + }; + addAndMakeVisible(enablePedalButton); + } else { + DBG("Erreur : image power.png introuvable ou invalide."); + } + + addAndMakeVisible(*pedalLabel); + addAndMakeVisible(*isEnabledIndicator); + + using Track = Grid::TrackInfo; + using Fr = Grid::Fr; + pedalLayout.templateRows = { + Track(Fr(2)), + Track(Fr(1)), + Track(Fr(1)), + Track(Fr(1)) + }; + pedalLayout.templateColumns = { + Track(Fr(1)) + }; + + pedalLayout.items = { + GridItem(*settingsLayout), + GridItem(enablePedalButton), + GridItem(*isEnabledIndicator), + GridItem(*pedalLabel), + }; +} \ No newline at end of file diff --git a/src/components/effects/DelayEffectComponent.cpp b/src/components/effects/DelayEffectComponent.cpp new file mode 100644 index 0000000..744780c --- /dev/null +++ b/src/components/effects/DelayEffectComponent.cpp @@ -0,0 +1,71 @@ +#include "DelayEffectComponent.h" + +DelayEffectComponent::DelayEffectComponent() : BasePedalComponent( + new DelayEffect()) {} + +DelayEffectComponent::DelayEffectComponent(AbstractEffect* effect) : + BasePedalComponent(effect) { + if (auto delayEffect = dynamic_cast(effect)) { + primaryColor = Colours::mediumblue; + using Track = Grid::TrackInfo; + using Fr = Grid::Fr; + grid.templateRows = {Track(Fr(1)), Track(Fr(3))}; + grid.templateColumns = {Track(Fr(1)), Track(Fr(1))}; + grid.items = { + GridItem(rateLabel), + GridItem(delayLabel), + GridItem(rateSlider), + GridItem(delaySlider), + }; + rateSlider.setSliderStyle(Slider::SliderStyle::RotaryVerticalDrag); + rateSlider.setTextBoxStyle(Slider::TextBoxBelow, false, 100, 20); + rateSlider.setColour(Slider::textBoxOutlineColourId, + Colours::transparentWhite); + rateSlider.setTextValueSuffix("%"); + rateSlider.setTitle("Rate"); + rateSlider.setRange(0.0, 100.0, 1); + rateSlider.setValue(50.0); + rateSlider.onValueChange = [this, delayEffect] { + delayEffect->setRate(rateSlider.getValue()); + }; + + rateLabel.setText("Rate", dontSendNotification); + rateLabel.setJustificationType(Justification::centred); + rateLabel.attachToComponent(&rateSlider, false); + + delaySlider.setSliderStyle(Slider::SliderStyle::RotaryVerticalDrag); + delaySlider.setTextValueSuffix("ms"); + delaySlider.setTitle("Delay"); + delaySlider.setTextBoxStyle(Slider::TextBoxBelow, false, 100, 20); + delaySlider.setColour(Slider::textBoxOutlineColourId, + Colours::transparentWhite); + delaySlider.setRange(0.0, 3000.0, 10); + delaySlider.setValue(500.0); + delaySlider.onValueChange = [this, delayEffect] { + delayEffect->setDelay(delaySlider.getValue()); + }; + + delayLabel.setText("Delay", dontSendNotification); + delayLabel.setJustificationType(Justification::centred); + delayLabel.attachToComponent(&delaySlider, false); + + rateSlider.setBounds(0, 0, 200, 200); + delaySlider.setBounds(300, 0, 200, 200); + rateLabel.setBounds(rateSlider.getX(), rateSlider.getY() + 20, + rateSlider.getWidth(), 20); + delayLabel.setBounds(delaySlider.getX(), delaySlider.getY() + 20, + delaySlider.getWidth(), 20); + + settingsLayout = new PedalSettingsLayoutComponent(&grid); + + addAndMakeVisible(rateSlider); + addAndMakeVisible(rateLabel); + addAndMakeVisible(delaySlider); + addAndMakeVisible(delayLabel); + + this->initializePedal(); + setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT); + } +} + +DelayEffectComponent::~DelayEffectComponent() = default; \ No newline at end of file diff --git a/src/components/effects/DistortionEffectComponent.cpp b/src/components/effects/DistortionEffectComponent.cpp new file mode 100644 index 0000000..a92b7c5 --- /dev/null +++ b/src/components/effects/DistortionEffectComponent.cpp @@ -0,0 +1,95 @@ +#include "DistortionEffect.h" +#include "DistortionEffectComponent.h" + +DistortionEffectComponent::DistortionEffectComponent() + : BasePedalComponent(new DistortionEffect()) {} + +DistortionEffectComponent::DistortionEffectComponent(AbstractEffect* effect) + : BasePedalComponent(effect) +{ + if (auto* distEffect = dynamic_cast(effect)) { + primaryColor = juce::Colours::darkviolet; + + // Level + levelSlider.setSliderStyle(juce::Slider::RotaryVerticalDrag); + levelSlider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 60, 20); + levelSlider.setColour(juce::Slider::textBoxOutlineColourId, juce::Colours::transparentWhite); + levelSlider.setRange(0.0, 1.0, 0.01); + levelSlider.setValue(distEffect->getLevel()); + levelSlider.onValueChange = [distEffect, this] { + distEffect->setLevel(static_cast(levelSlider.getValue())); + }; + levelLabel.setText("Level", juce::dontSendNotification); + levelLabel.attachToComponent(&levelSlider, false); + levelLabel.setJustificationType(juce::Justification::centred); + + // Tone + toneSlider.setSliderStyle(juce::Slider::RotaryVerticalDrag); + toneSlider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 60, 20); + toneSlider.setColour(juce::Slider::textBoxOutlineColourId, juce::Colours::transparentWhite); + toneSlider.setRange(0.0, 1.0, 0.01); + toneSlider.setValue(distEffect->getTone()); + toneSlider.onValueChange = [distEffect, this] { + distEffect->setTone(static_cast(toneSlider.getValue())); + }; + toneLabel.setText("Tone", juce::dontSendNotification); + toneLabel.attachToComponent(&toneSlider, false); + toneLabel.setJustificationType(juce::Justification::centred); + + // Dist + distSlider.setSliderStyle(juce::Slider::RotaryVerticalDrag); + distSlider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 60, 20); + distSlider.setColour(juce::Slider::textBoxOutlineColourId, juce::Colours::transparentWhite); + distSlider.setRange(0.0, 10.0, 0.01); + distSlider.setValue(distEffect->getDist()); + distSlider.onValueChange = [distEffect, this] { + distEffect->setDist(static_cast(distSlider.getValue())); + }; + distLabel.setText("Dist", juce::dontSendNotification); + distLabel.attachToComponent(&distSlider, false); + distLabel.setJustificationType(juce::Justification::centred); + + // Turbo + turboButton.setToggleState(distEffect->isTurbo(), juce::dontSendNotification); + turboButton.onClick = [distEffect, this] { + distEffect->setTurbo(turboButton.getToggleState()); + }; + + turboLabel.setText("Turbo", juce::dontSendNotification); + turboLabel.attachToComponent(&turboButton, false); + turboLabel.setJustificationType(juce::Justification::centred); + + // Layout + using Track = juce::Grid::TrackInfo; + using Fr = juce::Grid::Fr; + grid.templateRows = { Track (Fr (1)), Track (Fr (3)) }; + grid.templateColumns = { Track (Fr (1)), Track (Fr (1)), Track (Fr (1)), Track (Fr (1)) }; + grid.items = { + juce::GridItem(toneLabel), + juce::GridItem(distLabel), + juce::GridItem(levelLabel), + juce::GridItem(turboLabel), + juce::GridItem(toneSlider), + juce::GridItem(distSlider), + juce::GridItem(levelSlider), + juce::GridItem(turboButton) + }; + + settingsLayout = new PedalSettingsLayoutComponent(&grid); + + addAndMakeVisible(levelSlider); + addAndMakeVisible(levelLabel); + addAndMakeVisible(toneSlider); + addAndMakeVisible(toneLabel); + addAndMakeVisible(distSlider); + addAndMakeVisible(distLabel); + addAndMakeVisible(turboButton); + addAndMakeVisible(turboLabel); + + this->initializePedal(); + + setSize(400, DEFAULT_HEIGHT); + } +} + +DistortionEffectComponent::~DistortionEffectComponent() = default; \ No newline at end of file diff --git a/src/components/effects/EqualizerEffectComponent.cpp b/src/components/effects/EqualizerEffectComponent.cpp new file mode 100644 index 0000000..ab116e2 --- /dev/null +++ b/src/components/effects/EqualizerEffectComponent.cpp @@ -0,0 +1,72 @@ +#include "EqualizerEffectComponent.h" +#include + +#include "EqualizerEffectComponent.h" + + +EqualizerEffectComponent::EqualizerEffectComponent(AbstractEffect* effect) + : BasePedalComponent(effect), eqEffect(dynamic_cast(effect)) { + + jassert(eqEffect != nullptr); + + primaryColor = juce::Colours::orange; + + const char* freqLabels[10] = { "31Hz", "62Hz", "125Hz", "250Hz", "500Hz", "1k", "2k", "4k", "8k", "16k" }; + + using Track = juce::Grid::TrackInfo; + using Fr = juce::Grid::Fr; + + // 2 lignes : sliders + labels + grid.templateRows = { Track(Fr(1)), Track(Fr(3)) }; + grid.templateColumns = { Track(Fr(1)), Track(Fr(1)), Track(Fr(1)), Track(Fr(1)), Track(Fr(1)), + Track(Fr(1)), Track(Fr(1)), Track(Fr(1)), Track(Fr(1)), Track(Fr(1)) }; + + for (int i = 0; i < 10; ++i) { + sliders[i].setSliderStyle(juce::Slider::LinearVertical); + sliders[i].setTextBoxStyle(juce::Slider::TextBoxBelow, false, 50, 20); + sliders[i].setRange(-24.0, 24.0, 0.1); + sliders[i].setValue(0.0); + sliders[i].setColour(juce::Slider::textBoxOutlineColourId, juce::Colours::transparentWhite); + sliders[i].onValueChange = [this, i] + { + if (static_cast(sliders[i].getValue())) { + eqEffect->setGain(i, static_cast(sliders[i].getValue())); + } + }; + sliders[i].setTitle(freqLabels[i]); + sliders[i].setBounds(i * (getWidth() / 10), 0, getWidth() / 10, (getHeight() / 5) * 4); + + labels[i].setText(freqLabels[i], juce::dontSendNotification); + labels[i].setJustificationType(juce::Justification::centred); + labels[i].attachToComponent(&sliders[i], false); + labels[i].setBounds(i * (getWidth() / 10), 0, getWidth() / 10, getHeight() / 5); + + addAndMakeVisible(sliders[i]); + addAndMakeVisible(labels[i]); + } + + for (int i = 0; i < 10; ++i) + { + grid.items.add(GridItem(labels[i])); + } + for (int i = 0; i < 10; ++i) + { + grid.items.add(juce::GridItem(sliders[i])); + } + + settingsLayout = new PedalSettingsLayoutComponent(&grid); + + this->initializePedal(); + setSize(400, 300); +} + + + +void EqualizerEffectComponent::sliderValueChanged(juce::Slider* slider) { + for (int i = 0; i < 10; ++i) { + if (slider == &sliders[i]) { + eqEffect->setGain(i, slider->getValue()); + break; + } + } +} \ No newline at end of file diff --git a/src/components/effects/NoiseGateEffectComponent.cpp b/src/components/effects/NoiseGateEffectComponent.cpp new file mode 100644 index 0000000..0389c56 --- /dev/null +++ b/src/components/effects/NoiseGateEffectComponent.cpp @@ -0,0 +1,109 @@ +#include "NoiseGateEffectComponent.h" +#include "NoiseGateEffect.h" + +NoiseGateEffectComponent::NoiseGateEffectComponent(AbstractEffect* effect): BasePedalComponent(effect) +{ + if (NoiseGateEffect* noiseGateEffect = dynamic_cast(effect)) + { + primaryColor = juce::Colours::darkgrey; + using Track = juce::Grid::TrackInfo; + using Fr = juce::Grid::Fr; + + grid.templateRows = { Track(Fr(1)), Track(Fr(3)) }; + grid.templateColumns = { Track(Fr(1)), Track(Fr(1)), Track(Fr(1)) }; + + thresholdSlider.setSliderStyle(juce::Slider::RotaryVerticalDrag); + thresholdSlider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 100, 20); + thresholdSlider.setColour(juce::Slider::textBoxOutlineColourId, juce::Colours::transparentWhite); + thresholdSlider.setTextValueSuffix("dB"); + thresholdSlider.setTitle("Threshold"); + thresholdSlider.setRange(-100.0f, 0.0f, 0.1f); + thresholdSlider.setValue(-50.0f); + thresholdSlider.onValueChange = [this, noiseGateEffect] { + this->onThreholdValueChanged(noiseGateEffect, thresholdSlider.getValue()); + }; + + thresholdLabel.setText("Threshold", juce::dontSendNotification); + thresholdLabel.setJustificationType(juce::Justification::centred); + thresholdLabel.attachToComponent(&thresholdSlider, false); + + attackSlider.setSliderStyle(juce::Slider::RotaryVerticalDrag); + attackSlider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 100, 20); + attackSlider.setColour(juce::Slider::textBoxOutlineColourId, juce::Colours::transparentWhite); + attackSlider.setTextValueSuffix("ms"); + attackSlider.setTitle("Attack"); + attackSlider.setRange(0.0f, 100.0f, 0.1f); + attackSlider.setValue(10.0f); + attackSlider.onValueChange = [this, noiseGateEffect] { + this->onAttackValueChanged(noiseGateEffect, attackSlider.getValue()); + }; + + attackLabel.setText("Attack", juce::dontSendNotification); + attackLabel.setJustificationType(juce::Justification::centred); + attackLabel.attachToComponent(&attackSlider, false); + + releaseSlider.setSliderStyle(juce::Slider::RotaryVerticalDrag); + releaseSlider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 100, 20); + releaseSlider.setColour(juce::Slider::textBoxOutlineColourId, juce::Colours::transparentWhite); + releaseSlider.setTextValueSuffix("ms"); + releaseSlider.setTitle("Release"); + releaseSlider.setRange(0.0f, 1000.0f, 1.0f); + releaseSlider.setValue(100.0f); + releaseSlider.onValueChange = [this, noiseGateEffect] { + this->onReleaseValueChanged(noiseGateEffect, releaseSlider.getValue()); + }; + + releaseLabel.setText("Release", juce::dontSendNotification); + releaseLabel.setJustificationType(juce::Justification::centred); + releaseLabel.attachToComponent(&releaseSlider, false); + + grid.items = { + juce::GridItem(thresholdLabel), + juce::GridItem(attackLabel), + juce::GridItem(releaseLabel), + juce::GridItem(thresholdSlider), + juce::GridItem(attackSlider), + juce::GridItem(releaseSlider) + }; + + this->settingsLayout = new PedalSettingsLayoutComponent(&grid); + + addAndMakeVisible(thresholdSlider); + addAndMakeVisible(thresholdLabel); + addAndMakeVisible(attackSlider); + addAndMakeVisible(attackLabel); + addAndMakeVisible(releaseSlider); + addAndMakeVisible(releaseLabel); + + this->initializePedal(); + setSize(300, 300); + } +} + +NoiseGateEffectComponent::~NoiseGateEffectComponent() = default; + +void NoiseGateEffectComponent::onThreholdValueChanged(NoiseGateEffect* effect, double value) +{ + if (float threshold = static_cast(value)) + { + effect->setThreshold(threshold); + } +} + +void NoiseGateEffectComponent::onAttackValueChanged(NoiseGateEffect* effect, double value) +{ + if (float attack = static_cast(value)) + { + effect->setAttack(attack); + } +} + +void NoiseGateEffectComponent::onReleaseValueChanged(NoiseGateEffect* effect, double value) +{ + if (float release = static_cast(value)) + { + effect->setRelease(release); + } +} + + diff --git a/src/effects/ChorusEffect.cpp b/src/effects/ChorusEffect.cpp new file mode 100644 index 0000000..c1c0cac --- /dev/null +++ b/src/effects/ChorusEffect.cpp @@ -0,0 +1,93 @@ +#include "ChorusEffect.h" + +ChorusEffect::ChorusEffect() { + effectName = "Chorus"; + updateParameters(); +} + +void ChorusEffect::apply(const juce::AudioSourceChannelInfo& bufferToFill) { + auto* buffer = bufferToFill.buffer; + if (buffer == nullptr || buffer->getNumSamples() == 0 || buffer->getNumChannels() == 0) + return; + + juce::dsp::AudioBlock block(*buffer); + juce::dsp::ProcessContextReplacing context(block); + + chorus.process(context); +} + + +void ChorusEffect::updateParameters() { + chorus.setRate(rate); + chorus.setDepth(depth); + chorus.setCentreDelay(centreDelay); + chorus.setFeedback(feedback); + chorus.setMix(mix); +} + +float ChorusEffect::getDepth() const { return depth; } +float ChorusEffect::getRate() const { return rate; } +float ChorusEffect::getMix() const { return mix; } + +void ChorusEffect::setRate(float newRate) { + rate = newRate; + chorus.setRate(rate); +} + +void ChorusEffect::setDepth(float newDepth) { + depth = newDepth; + chorus.setDepth(depth); +} + +void ChorusEffect::setCentreDelay(float newCentreDelay) { + centreDelay = newCentreDelay; + chorus.setCentreDelay(centreDelay); +} + +void ChorusEffect::setFeedback(float newFeedback) { + feedback = newFeedback; + chorus.setFeedback(feedback); +} + +void ChorusEffect::setMix(float newMix) { + mix = newMix; + chorus.setMix(mix); +} + +void ChorusEffect::prepare(double sampleRate, int samplesPerBlock, int numChannels) +{ + juce::dsp::ProcessSpec spec; + spec.sampleRate = sampleRate; + spec.maximumBlockSize = static_cast(samplesPerBlock); + spec.numChannels = static_cast(numChannels); + + chorus.prepare(spec); +} + + +bool ChorusEffect::operator==(const AbstractEffect* effect) { + return this == effect; +} + +juce::var ChorusEffect::toJSON() const { + auto obj = AbstractEffect::toJSON(); + if (auto* dynamicObj = obj.getDynamicObject()) { + dynamicObj->setProperty("rate", rate); + dynamicObj->setProperty("depth", depth); + dynamicObj->setProperty("centreDelay", centreDelay); + dynamicObj->setProperty("feedback", feedback); + dynamicObj->setProperty("mix", mix); + } + return obj; +} + +void ChorusEffect::fromJSON(const juce::var& json) { + AbstractEffect::fromJSON(json); + if (const auto* obj = json.getDynamicObject()) { + if (obj->hasProperty("rate")) setRate((float)obj->getProperty("rate")); + if (obj->hasProperty("depth")) setDepth((float)obj->getProperty("depth")); + if (obj->hasProperty("centreDelay")) setCentreDelay((float)obj->getProperty("centreDelay")); + if (obj->hasProperty("feedback")) setFeedback((float)obj->getProperty("feedback")); + if (obj->hasProperty("mix")) setMix((float)obj->getProperty("mix")); + } +} diff --git a/src/effects/DelayEffect.cpp b/src/effects/DelayEffect.cpp new file mode 100644 index 0000000..fd19713 --- /dev/null +++ b/src/effects/DelayEffect.cpp @@ -0,0 +1,48 @@ +#include "DelayEffect.h" + +DelayEffect::DelayEffect() { + rate = 50.0; + delay = 500.0; + effectName = "Delay"; +} + +DelayEffect::~DelayEffect() = default; + +void DelayEffect::apply(const AudioSourceChannelInfo &bufferToFill) { + if (delay > 0 && rate > 0) { + const int numChannels = bufferToFill.buffer->getNumChannels(); + const int numSamples = bufferToFill.numSamples; + constexpr auto sampleRate = 44100.0; + const auto delaySamples = static_cast(delay * sampleRate / 1000.0f); + + if (circularBuffer.size() < delaySamples) { + circularBuffer.resize(delaySamples, 0.0f); + writePosition = 0; + } + + + for (int i = 0; i < numSamples; ++i) { + auto delayedSample = circularBuffer[writePosition]; + for (int channel = 0; channel < numChannels; ++channel) + { + auto* channelBuffer = bufferToFill.buffer->getWritePointer(channel); + auto inputSample = channelBuffer[i]; + channelBuffer[i] = inputSample + delayedSample * (rate / 100.0f); + circularBuffer[writePosition] = inputSample; + } + writePosition = (writePosition + 1) % delaySamples; + } + } +} + +void DelayEffect::setRate(const float rate) { + this->rate = rate; +} + +void DelayEffect::setDelay(const float delay) { + this->delay = delay; +} + +bool DelayEffect::operator==(const AbstractEffect* effect) { + return this == effect; +} diff --git a/src/effects/DistortionEffect.cpp b/src/effects/DistortionEffect.cpp new file mode 100644 index 0000000..c54b1e1 --- /dev/null +++ b/src/effects/DistortionEffect.cpp @@ -0,0 +1,90 @@ +#include "DistortionEffect.h" +#include + +float gDrive = 1.0f; +bool gTurbo = false; + +DistortionEffect::DistortionEffect() { + effectName = "Distortion"; + updateWaveshaper(); + updateTone(); + processorChain.get<3>().setGainLinear(level); +} + +DistortionEffect::~DistortionEffect() = default; + +void DistortionEffect::prepare(const juce::dsp::ProcessSpec& spec) { + processorChain.prepare(spec); + // High-pass à 120 Hz + *processorChain.get<0>().coefficients = *juce::dsp::IIR::Coefficients::makeHighPass(spec.sampleRate, 120.0f); + updateTone(); +} + +void DistortionEffect::reset() noexcept { + processorChain.reset(); +} + +void DistortionEffect::apply(const juce::AudioSourceChannelInfo& bufferToFill) { + if (bufferToFill.buffer == nullptr) return; + juce::dsp::AudioBlock block(*bufferToFill.buffer, (size_t) bufferToFill.startSample); + const int numChannels = block.getNumChannels(); + // auto subBlock = block.getSubBlock(0, (size_t) bufferToFill.numSamples); + // juce::dsp::ProcessContextReplacing context(subBlock); + for (int channel = 0; channel < numChannels; ++channel) + { + auto channelBlock = block.getSingleChannelBlock((size_t)channel); + juce::dsp::ProcessContextReplacing context(channelBlock); + processorChain.process(context); + } +} + +// --- Paramètres --- + +void DistortionEffect::setLevel(float value) { + level = juce::jlimit(0.0f, 1.0f, value); + processorChain.get<3>().setGainLinear(level); +} +void DistortionEffect::setTone(float value) { + tone = juce::jlimit(0.0f, 1.0f, value); + updateTone(); +} +void DistortionEffect::setDist(float value) { + dist = juce::jlimit(0.0f, 10.0f, value); + updateWaveshaper(); +} +void DistortionEffect::setTurbo(bool enabled) { + turbo = enabled; + updateWaveshaper(); + updateTone(); +} + +float DistortionEffect::getLevel() const { return level; } +float DistortionEffect::getTone() const { return tone; } +float DistortionEffect::getDist() const { return dist; } +bool DistortionEffect::isTurbo() const { return turbo; } + +// --- DSP interne --- + +void DistortionEffect::updateTone() { + // Tone = low-pass variable, Turbo = plus d'aigus + float freq = 2000.0f + tone * (turbo ? 8000.0f : 4000.0f); // 2kHz à 10kHz (Turbo) ou 2kHz à 6kHz + *processorChain.get<2>().coefficients = *juce::dsp::IIR::Coefficients::makeLowPass(44100, freq); +} + +float DistortionEffect::waveshaperFunc(float x) { + // Utilise les membres statiques pour le drive/turbo + // (Attention, non thread-safe, mais simple pour un test) + extern float gDrive; + extern bool gTurbo; + if (gTurbo) + return std::tanh(gDrive * x) * 1.2f; + else + return std::tanh(gDrive * x); +} + +void DistortionEffect::updateWaveshaper() { + float drive = 1.0f + dist * (turbo ? 100.0f : 50.0f); // Turbo = plus de gain + gDrive = drive; + gTurbo = turbo; + processorChain.get<1>().functionToUse = &DistortionEffect::waveshaperFunc; +} \ No newline at end of file diff --git a/src/effects/EffectsFactory.cpp b/src/effects/EffectsFactory.cpp new file mode 100644 index 0000000..c9261f3 --- /dev/null +++ b/src/effects/EffectsFactory.cpp @@ -0,0 +1,16 @@ +#include "EffectsFactory.h" +#include "DelayEffect.h" +#include "DistortionEffect.h" +#include "EqualizerEffect.h" +#include "NoiseGateEffect.h" +#include "ChorusEffect.h" + +std::vector EffectsFactory::createAllEffects() { + std::vector effects; + effects.push_back(new DelayEffect()); + effects.push_back(new DistortionEffect()); + effects.push_back(new EqualizerEffect()); + effects.push_back(new NoiseGateEffect()); + effects.push_back(new ChorusEffect()); + return effects; +} diff --git a/src/effects/EqualizerEffect.cpp b/src/effects/EqualizerEffect.cpp new file mode 100644 index 0000000..db7d960 --- /dev/null +++ b/src/effects/EqualizerEffect.cpp @@ -0,0 +1,78 @@ +#include "EqualizerEffect.h" +#include + +EqualizerEffect::EqualizerEffect() { + effectName = "Equalizer"; + filters.resize(numBands); + for (int i = 0; i < numBands; ++i) bandGains[i] = 0.0f; + updateFilters(); +} + +void EqualizerEffect::apply(const juce::AudioSourceChannelInfo &bufferToFill) { + auto* buffer = bufferToFill.buffer; + const int numChannels = buffer->getNumChannels(); + const int numSamples = bufferToFill.numSamples; + + juce::dsp::AudioBlock block(*buffer); + + for (int channel = 0; channel < numChannels; ++channel) { + auto channelBlock = block.getSingleChannelBlock((size_t)channel); + juce::dsp::ProcessContextReplacing context(channelBlock); + + for (int i = 0; i < numBands; ++i) { + filters[i].process(context); + } + } +} + +void EqualizerEffect::setGain(int bandIndex, float gain) { + if (bandIndex >= 0 && bandIndex < numBands) { + bandGains[bandIndex] = gain; + updateFilters(); + } +} + +float EqualizerEffect::getGain(int bandIndex) const { + return (bandIndex >= 0 && bandIndex < numBands) ? bandGains[bandIndex] : 0.0f; +} + +void EqualizerEffect::updateFilters() { + // Fréquences fixes typiques d'un égaliseur 10 bandes + const float freqs[10] = {31, 62, 125, 250, 500, 1000, 2000, 4000, 8000, 16000}; + constexpr float sampleRate = 44100.0f; + + for (int i = 0; i < numBands; ++i) { + filters[i].reset(); + auto coeffs = juce::dsp::IIR::Coefficients::makePeakFilter( + sampleRate, freqs[i], 1.0f, juce::Decibels::decibelsToGain(bandGains[i]) + ); + filters[i].coefficients = coeffs; + } +} + +bool EqualizerEffect::operator==(const AbstractEffect* effect) { + return this == effect; +} + +juce::var EqualizerEffect::toJSON() const { + auto obj = AbstractEffect::toJSON(); + if (auto *dynamicObj = obj.getDynamicObject()) { + juce::Array gains; + for (float g : bandGains) gains.add(g); + dynamicObj->setProperty("bandGains", gains); + } + return obj; +} + +void EqualizerEffect::fromJSON(const juce::var &json) { + AbstractEffect::fromJSON(json); + if (const auto* obj = json.getDynamicObject()) { + const auto& gains = obj->getProperty("bandGains"); + if (auto* arr = gains.getArray()) { + for (int i = 0; i < juce::jmin((int)arr->size(), numBands); ++i) { + bandGains[i] = static_cast((*arr)[i]); + } + updateFilters(); + } + } +} diff --git a/src/effects/NoiseGateEffect.cpp b/src/effects/NoiseGateEffect.cpp new file mode 100644 index 0000000..6bbe2ab --- /dev/null +++ b/src/effects/NoiseGateEffect.cpp @@ -0,0 +1,57 @@ +#include "NoiseGateEffect.h" + +NoiseGateEffect::NoiseGateEffect() { + threshold = 0.5f; + attack = 0.01f; + release = 0.1f; + effectName = "Noise Gate"; +} + +NoiseGateEffect::~NoiseGateEffect() = default; + +void NoiseGateEffect::apply(const AudioSourceChannelInfo &bufferToFill) { + auto *leftBuffer = bufferToFill.buffer->getWritePointer(0, bufferToFill.startSample); + auto *rightBuffer = bufferToFill.buffer->getNumChannels() > 1 + ? bufferToFill.buffer->getWritePointer(1, bufferToFill.startSample) + : nullptr; + int numSamples = bufferToFill.numSamples; + + float envelopeLeft = 0.0f; + float envelopeRight = 0.0f; + + float attackCoeff = std::exp(-1.0f / (attack * numSamples)); + float releaseCoeff = std::exp(-1.0f / (release * numSamples)); + + for (int i = 0; i < numSamples; ++i) { + float inputSampleLeft = leftBuffer[i]; + float inputSampleRight = rightBuffer ? rightBuffer[i] : 0.0f; + + envelopeLeft = std::max(std::abs(inputSampleLeft), envelopeLeft * (inputSampleLeft > envelopeLeft ? attackCoeff : releaseCoeff)); + if (rightBuffer) + envelopeRight = std::max(std::abs(inputSampleRight), envelopeRight * (inputSampleRight > envelopeRight ? attackCoeff : releaseCoeff)); + + float gainLeft = envelopeLeft < threshold ? 0.0f : 1.0f; + float gainRight = rightBuffer ? (envelopeRight < threshold ? 0.0f : 1.0f) : 1.0f; + + leftBuffer[i] = inputSampleLeft * gainLeft; + if (rightBuffer) { + rightBuffer[i] = inputSampleRight * gainRight; + } + } +} + +void NoiseGateEffect::setThreshold(float threshold) { + this->threshold = threshold; +} + +void NoiseGateEffect::setAttack(float attack) { + this->attack = attack; +} + +void NoiseGateEffect::setRelease(float release) { + this->release = release; +} + +bool NoiseGateEffect::operator==(const AbstractEffect* effect) { + return this == effect; +} \ No newline at end of file diff --git a/src/models/Pedal.cpp b/src/models/Pedal.cpp new file mode 100644 index 0000000..52e2996 --- /dev/null +++ b/src/models/Pedal.cpp @@ -0,0 +1,5 @@ +// +// Created by Ema on 14/05/2025. +// + +#include "Pedal.h" diff --git a/src/utils/EffectSerializer.cpp b/src/utils/EffectSerializer.cpp new file mode 100644 index 0000000..990c1a2 --- /dev/null +++ b/src/utils/EffectSerializer.cpp @@ -0,0 +1,80 @@ +#pragma once + +#include +#include +#include +#include "AbstractEffect.h" +#include "DelayEffect.h" + +class EffectSerializer { +public: + /** + * @brief Saves a list of effects to a JSON file. + * @param effects Vector of effect pointers to serialize + * @param filePath Path to save the JSON file + * @return True if saving succeeded, false otherwise + */ + static bool saveEffectsToFile(const std::vector > &effects, const File &filePath) { + const auto effectsArray = var(Array()); + Array *array = effectsArray.getArray(); + + for (const auto &effect: effects) { + array->add(effect->toJSON()); + } + + DynamicObject *root = new DynamicObject(); + root->setProperty("effects", effectsArray); + + String jsonString = JSON::toString(root); + return filePath.replaceWithText(jsonString); + } + + /** + * @brief Loads effects from a JSON file. + * @param filePath Path to the JSON file + * @return Vector of loaded effect pointers + */ + static std::vector > loadEffectsFromFile(const File &filePath) { + std::vector > loadedEffects; + + if (!filePath.existsAsFile()) { + return loadedEffects; + } + + const String jsonContent = filePath.loadFileAsString(); + const auto rootVar = JSON::parse(jsonContent); + + if (const auto *root = rootVar.getDynamicObject()) { + const auto effectsVar = root->getProperty("effects"); + + if (auto *effectsArray = effectsVar.getArray()) { + for (const auto &effectVar: *effectsArray) { + if (const auto *effectObj = effectVar.getDynamicObject()) { + String type = effectObj->getProperty("type").toString(); + + if (std::shared_ptr effect = createEffectFromType(type)) { + effect->fromJSON(effectVar); + loadedEffects.push_back(effect); + } + } + } + } + } + + return loadedEffects; + } + +private: + /** + * @brief Creates an effect instance based on its type string. + * @param type The type name of the effect + * @return Shared pointer to the created effect, or nullptr if type is unknown + */ + static std::shared_ptr createEffectFromType(const String &type) { + if (type == "DelayEffect") + return std::make_shared(); + // Add more effect types here as needed + + return nullptr; + } +}; diff --git a/src/utils/ModalOverlayComponent.cpp b/src/utils/ModalOverlayComponent.cpp new file mode 100644 index 0000000..936a1a5 --- /dev/null +++ b/src/utils/ModalOverlayComponent.cpp @@ -0,0 +1,56 @@ +#include "ModalOverlayComponent.h" + +#include "ResourceManager.h" + +ModalOverlayComponent::ModalOverlayComponent(std::string viewName, juce::Component* modalContent) +{ + addAndMakeVisible(modalComponent = modalContent); + this->setInterceptsMouseClicks(true, false); + + modalComponent->setInterceptsMouseClicks(true, true); + modalComponent->setCentreRelative(0.5f, 0.5f); + + addAndMakeVisible(viewNameLabel); + viewNameLabel.setText(viewName, juce::dontSendNotification); + viewNameLabel.setFont(juce::FontOptions(32.0f, juce::Font::bold)); + viewNameLabel.setJustificationType(juce::Justification::centred); + + juce::Image closeImage = ResourceManager::loadImage("resources/icons/xmark.png"); + if (closeImage.isValid()) { + closeOverlayButton.setImages(true, true, true,closeImage, 1.0f, {}, closeImage, 1.0f, {},closeImage, 1.0f, {}); + closeOverlayButton.setSize(closeImage.getWidth(), closeImage.getHeight()); + addAndMakeVisible(closeOverlayButton); + } else { + DBG("Erreur : image xmark.png introuvable ou invalide."); + } + closeOverlayButton.onClick = [this]() { this->onCloseOverlayButtonClicked(); }; +} + +void ModalOverlayComponent::resized() +{ + viewNameLabel.setBounds(0, 0, getWidth(), 50); + closeOverlayButton.setBounds(getWidth() - 100, 10, 100, 50); + if (modalComponent != nullptr) + { + modalComponent->setBounds(getLocalBounds()); + } +} + +void ModalOverlayComponent::paint(juce::Graphics& g) +{ + g.fillAll(juce::Colours::black.withAlpha(0.8f)); +} + +void ModalOverlayComponent::onCloseOverlayButtonClicked() +{ + if (modalComponent != nullptr) + { + removeAllChildren(); + delete modalComponent; + auto* mainWindow = getTopLevelComponent(); + if (mainWindow == nullptr) + return; + mainWindow->removeChildComponent(this); + delete this; + } +} diff --git a/src/utils/ResourceManager.cpp b/src/utils/ResourceManager.cpp new file mode 100644 index 0000000..e6ef989 --- /dev/null +++ b/src/utils/ResourceManager.cpp @@ -0,0 +1,28 @@ +#include "ResourceManager.h" + +ResourceManager::ResourceManager() = default; +ResourceManager::~ResourceManager() = default; + +Image ResourceManager::loadImage(const String& relativePath) +{ + File imageFile; + +#if JUCE_MAC + imageFile = File::getSpecialLocation(File::currentApplicationFile) + .getParentDirectory() + .getParentDirectory() + .getParentDirectory() + .getChildFile(relativePath); +#else + imageFile = juce::File::getCurrentWorkingDirectory().getChildFile(relativePath); +#endif + + Image image = ImageFileFormat::loadFrom(imageFile); + + if (image.isNull()) + { + DBG("Erreur : impossible de charger l'image à partir de " + imageFile.getFullPathName()); + } + + return image; // retourne un objet juce::Image valide ou invalide (mais jamais nullptr) +} diff --git a/src/utils/ToggleButtonComponent.cpp b/src/utils/ToggleButtonComponent.cpp new file mode 100644 index 0000000..07b3baf --- /dev/null +++ b/src/utils/ToggleButtonComponent.cpp @@ -0,0 +1,42 @@ +#include "ToggleButtonComponent.h" + +namespace { +constexpr int buttonWidth = 100; +constexpr int buttonHeight = 50; +constexpr float toggleWidth = 50.0f; +constexpr float toggleHeight = 25.0f; +constexpr float toggleRadius = 10.0f; +constexpr float ellipseDiameter = 19.0f; +constexpr float ellipseYOffset = 3.0f; +constexpr float ellipseXOffsetOn = 0.0f; +constexpr float ellipseXOffsetOff = -22.0f; +} + +ToggleButtonComponent::ToggleButtonComponent() { + setMouseCursor(MouseCursor::PointingHandCursor); + setSize(buttonWidth, buttonHeight); +} + +ToggleButtonComponent::~ToggleButtonComponent() = default; + +void ToggleButtonComponent::paint(Graphics& g) { + const auto centerX = static_cast(getWidth()) / 2.0f; + const auto centerY = static_cast(getHeight()) / 2.0f; + + g.setColour(Colours::darkgrey); + g.fillRoundedRectangle(centerX - toggleWidth / 2.0f, + centerY - toggleHeight / 2.0f, + toggleWidth, toggleHeight, toggleRadius); + + if (getToggleState()) { + g.setColour(Colours::green); + g.fillEllipse(centerX + ellipseXOffsetOn, + centerY - toggleHeight / 2.0f + ellipseYOffset, + ellipseDiameter, ellipseDiameter); + } else { + g.setColour(Colours::red); + g.fillEllipse(centerX + ellipseXOffsetOff, + centerY - toggleHeight / 2.0f + ellipseYOffset, + ellipseDiameter, ellipseDiameter); + } +} \ No newline at end of file