├── .gitattributes ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── Midi Transposer v0.5.png ├── ProjectInfo.h.in ├── README.md ├── data ├── config │ ├── colors.json │ └── positions.json ├── fonts │ └── iosevka-regular.ttf └── img │ ├── copy.svg │ ├── delete.svg │ ├── new.svg │ ├── next.svg │ ├── previous.svg │ └── save.svg ├── get_cpm.cmake └── src ├── gui ├── CompColors.h ├── CompCoordinates.h ├── Configuration.hpp ├── PluginEditor.cpp ├── PluginEditor.h ├── UISettings.h ├── lookandfeel │ ├── BaseLookAndFeel.cpp │ └── BaseLookAndFeel.h ├── panels │ ├── ArpPanel.cpp │ ├── ArpPanel.h │ ├── IntervalsPanel.cpp │ ├── IntervalsPanel.h │ ├── KeysPanel.cpp │ ├── KeysPanel.h │ ├── MainPanel.cpp │ ├── MainPanel.h │ ├── MidiPanel.cpp │ ├── MidiPanel.h │ ├── PresetsPanel.cpp │ └── PresetsPanel.h └── widgets │ ├── CustomSlider.cpp │ ├── CustomSlider.h │ ├── CustomToggleButton.cpp │ ├── CustomToggleButton.h │ ├── Helpers.h │ ├── NoteKey.cpp │ ├── NoteKey.h │ ├── TextSwitch.cpp │ └── TextSwitch.h ├── params ├── ArpeggiatorParams.cpp ├── IntervalParam.cpp ├── MidiParams.cpp ├── NoteParam.cpp ├── NoteParams.cpp └── Params.h ├── presets ├── PresetManager.cpp └── PresetManager.h └── processor ├── MidiProcessor.cpp ├── MidiProcessor.h ├── PluginProcessor.cpp └── PluginProcessor.h /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache/ 2 | .idea/ 3 | .vscode/ 4 | .vs/ 5 | .DS_Store 6 | 7 | libs/ 8 | build/ 9 | out/ 10 | 11 | cmake-build-debug/ 12 | cmake-build-release/ 13 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.27) 2 | 3 | set(PROJECT_VERSION 0.6) 4 | 5 | project(MidiTransposer 6 | LANGUAGES CXX C 7 | VERSION ${PROJECT_VERSION}) 8 | 9 | if(APPLE) 10 | set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64") 11 | endif() 12 | 13 | if(MSVC) 14 | string(REGEX REPLACE "/W3" "" CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS}) 15 | string(REGEX REPLACE "-W3" "" CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS}) 16 | set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") 17 | endif() 18 | 19 | set(CMAKE_CXX_STANDARD 17) 20 | set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE) 21 | 22 | # Set any of these to "ON" if you want to build one of the juce examples 23 | # or extras (Projucer/AudioPluginHost, etc): 24 | option(JUCE_BUILD_EXTRAS "Build JUCE Extras" ON) 25 | option(JUCE_BUILD_EXAMPLES "Build JUCE Examples" OFF) 26 | 27 | # Adds all the module sources so they appear correctly in the IDE 28 | set(JUCE_ENABLE_MODULE_SOURCE_GROUPS "Enable Module Source Groups" ON) 29 | set_property(GLOBAL PROPERTY USE_FOLDERS YES) 30 | 31 | set(LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/libs) 32 | 33 | include(get_cpm.cmake) 34 | 35 | # Include JUCE from the Git repository 36 | CPMAddPackage( 37 | NAME JUCE 38 | GIT_TAG 7.0.12 39 | GITHUB_REPOSITORY juce-framework/JUCE 40 | SOURCE_DIR ${LIB_DIR}/juce 41 | ) 42 | 43 | # Add external libraries 44 | CPMAddPackage( 45 | NAME json 46 | GIT_TAG v3.11.3 47 | GITHUB_REPOSITORY nlohmann/json 48 | SOURCE_DIR ${LIB_DIR}/json 49 | ) 50 | 51 | CPMAddPackage(NAME Gin 52 | GITHUB_REPOSITORY FigBug/Gin 53 | DOWNLOAD_ONLY TRUE 54 | SOURCE_DIR ${LIB_DIR}/gin 55 | GIT_TAG master) 56 | 57 | # Manually add the gin module (the others are not required) 58 | juce_add_module(${LIB_DIR}/gin/modules/gin) 59 | 60 | # Generate the ProjectInfo struct. 61 | set(PROJECT_COMPANY "Stfufane") 62 | set(PROJECT_VERSION_STRING "${PROJECT_VERSION}") 63 | set(PROJECT_VERSION_NUMBER 0x00060000) 64 | configure_file(ProjectInfo.h.in data/ProjectInfo.h) 65 | include_directories(${CMAKE_CURRENT_BINARY_DIR}/data) 66 | 67 | juce_add_plugin("${PROJECT_NAME}" 68 | COMPANY_NAME "${PROJECT_COMPANY}" 69 | IS_SYNTH FALSE 70 | NEEDS_MIDI_INPUT TRUE 71 | NEEDS_MIDI_OUTPUT TRUE 72 | IS_MIDI_EFFECT TRUE 73 | EDITOR_WANTS_KEYBOARD_FOCUS FALSE 74 | COPY_PLUGIN_AFTER_BUILD FALSE 75 | PLUGIN_MANUFACTURER_CODE Stfu 76 | PLUGIN_CODE Mdtr 77 | FORMATS VST3 78 | PRODUCT_NAME "Midi Transposer") 79 | 80 | # List the sources files to compile 81 | target_sources(${PROJECT_NAME} PRIVATE 82 | src/processor/MidiProcessor.h 83 | src/processor/MidiProcessor.cpp 84 | src/processor/PluginProcessor.h 85 | src/processor/PluginProcessor.cpp 86 | src/params/Params.h 87 | src/params/ArpeggiatorParams.cpp 88 | src/params/IntervalParam.cpp 89 | src/params/MidiParams.cpp 90 | src/params/NoteParam.cpp 91 | src/params/NoteParams.cpp 92 | src/presets/PresetManager.h 93 | src/presets/PresetManager.cpp 94 | src/gui/PluginEditor.h 95 | src/gui/PluginEditor.cpp 96 | src/gui/CompColors.h 97 | src/gui/CompCoordinates.h 98 | src/gui/Configuration.hpp 99 | src/gui/UISettings.h 100 | src/gui/lookandfeel/BaseLookAndFeel.h 101 | src/gui/lookandfeel/BaseLookAndFeel.cpp 102 | src/gui/widgets/CustomSlider.h 103 | src/gui/widgets/CustomSlider.cpp 104 | src/gui/widgets/CustomToggleButton.h 105 | src/gui/widgets/CustomToggleButton.cpp 106 | src/gui/widgets/NoteKey.h 107 | src/gui/widgets/NoteKey.cpp 108 | src/gui/widgets/TextSwitch.h 109 | src/gui/widgets/TextSwitch.cpp 110 | src/gui/widgets/Helpers.h 111 | src/gui/panels/MainPanel.h 112 | src/gui/panels/MainPanel.cpp 113 | src/gui/panels/MidiPanel.h 114 | src/gui/panels/MidiPanel.cpp 115 | src/gui/panels/ArpPanel.h 116 | src/gui/panels/ArpPanel.cpp 117 | src/gui/panels/IntervalsPanel.h 118 | src/gui/panels/IntervalsPanel.cpp 119 | src/gui/panels/KeysPanel.h 120 | src/gui/panels/KeysPanel.cpp 121 | src/gui/panels/PresetsPanel.h 122 | src/gui/panels/PresetsPanel.cpp) 123 | 124 | # Build the binary data 125 | set(DataTarget "${PROJECT_NAME}-Data") 126 | juce_add_binary_data(${DataTarget} SOURCES 127 | ${CMAKE_CURRENT_LIST_DIR}/data/fonts/iosevka-regular.ttf 128 | ${CMAKE_CURRENT_LIST_DIR}/data/img/new.svg 129 | ${CMAKE_CURRENT_LIST_DIR}/data/img/save.svg 130 | ${CMAKE_CURRENT_LIST_DIR}/data/img/copy.svg 131 | ${CMAKE_CURRENT_LIST_DIR}/data/img/delete.svg 132 | ${CMAKE_CURRENT_LIST_DIR}/data/img/previous.svg 133 | ${CMAKE_CURRENT_LIST_DIR}/data/img/next.svg) 134 | 135 | target_link_libraries(${PROJECT_NAME} PRIVATE ${DataTarget}) 136 | 137 | # Set the base for includes to avoid having to use relative paths 138 | target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src) 139 | 140 | target_compile_definitions(${PROJECT_NAME} 141 | PUBLIC 142 | JUCE_WEB_BROWSER=0 143 | JUCE_USE_CURL=0 144 | DONT_SET_USING_JUCE_NAMESPACE=1 145 | JUCE_VST3_CAN_REPLACE_VST2=0 146 | CONFIG_FOLDER="${CMAKE_CURRENT_LIST_DIR}/data/config") 147 | 148 | target_link_libraries(${PROJECT_NAME} PRIVATE 149 | juce::juce_audio_utils 150 | juce::juce_recommended_config_flags 151 | juce::juce_recommended_lto_flags 152 | juce::juce_recommended_warning_flags 153 | gin 154 | nlohmann_json::nlohmann_json) 155 | 156 | if (MSVC) 157 | target_compile_options(${PROJECT_NAME} PRIVATE /Wall /WX) 158 | else() 159 | target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic) 160 | endif() 161 | 162 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Stéphane Albanese 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Midi Transposer v0.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stfufane/Midi-Transposer/a6f56c0b5cccbdf3bf40ac2e383becd9ee1e44e4/Midi Transposer v0.5.png -------------------------------------------------------------------------------- /ProjectInfo.h.in: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | struct ProjectInfo 4 | { 5 | static const char* const projectName; 6 | static const char* const companyName; 7 | static const char* const versionString; 8 | static const int versionNumber; 9 | }; 10 | 11 | const char* const ProjectInfo::projectName = "@PROJECT_NAME@"; 12 | const char* const ProjectInfo::companyName = "@PROJECT_COMPANY@"; 13 | const char* const ProjectInfo::versionString = "@PROJECT_VERSION_STRING@"; 14 | const int ProjectInfo::versionNumber = @PROJECT_VERSION_NUMBER@; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Midi Transposer 2 | 3 | 4 | A VST3 plugin developed with JUCE framework to map chords to a MIDI bass pedal (or any MIDI controller) 5 | 6 | ## General purpose 7 | 8 | 9 | The idea is to use my MIDI bass pedal (Roland PK5-A) and expand its usage by mapping a root note and up to 12 intervals to each note of the octave. 10 | This way I can build a sequence of chords to play with my feet while improvising on any other instrument. 11 | 12 | 13 | It was mainly a way to start learning how to use the JUCE framework and make an application I can use instead of a dull tutorial. 14 | I'll improve it as ideas come and I make progress learning the framework. 15 | 16 | It uses version 7.0.10 of JUCE by pulling the master branch from CMake. It builds and run on Windows 10/11, MacOS and Arch Linux so far. 17 | 18 | You're welcome to clone it and use it as you want. And if you see stupid code, don't hesitate to tell me, I'll be happy to improve and fix it :) 19 | 20 | ## Build 21 | 22 | ### CMake ### 23 | 24 | To build the project using CMake, use the command line 25 | 26 | ```bash 27 | # Generate 28 | cmake . -B build 29 | # Build 30 | cmake --build build # --config Release 31 | ``` 32 | 33 | If you want the plugin to be copied in your VST3 folder, change `COPY_PLUGIN_AFTER_BUILD` to TRUE in Source/CMakeLists.txt 34 | 35 | If you don't need JUCE extras (I use AudioPluginHost for debugging in Visual Studio Code), pass the `JUCE_BUILD_EXTRAS` option to OFF in the root CMakeLists.txt 36 | 37 | For a great guide about configuring a CMake project with Visual Studio Code, I advise you to check [this great repository](https://github.com/tomoyanonymous/juce_cmake_vscode_example). 38 | 39 | ## Features 40 | 41 | The interface is almost complete with this design. 42 | ![interface](./Midi%20Transposer%20v0.5.png) 43 | 44 | * **Input Midi Channel** : Defines from which channel the incoming MIDI events will be processed 45 | * **Output Midi Channel** : Defines to which channel the processed MIDI events will be routed 46 | * **Octave Transpose** : If a value other than 0 is selected, the root note will be played at its original height and the selected chord will be transposed to the chosen octave 47 | * **Arpeggiator** : Self explanatory :) 48 | 49 | When you click on a note, you have access to the intervals panel. 50 | 51 | There you can select which intervals will be played for the selected note. You can also decide to transpose the whole chord by a number of semitones. To close the panel, click again on the note. 52 | 53 | And that's it, it's pretty straight forward. 54 | 55 | ## Hot Reload and configuration 56 | 57 | The project uses [Gin](https://github.com/FigBug/Gin) for its filewatcher class that allows to configure some GUI values with hot reload. If you want to change some colors, you can edit data/config/colors.json and define different ones. 58 | 59 | The values in data/config/positions.json were mostly helpful during the development phase to define the different block sizes and positions and tweak all the coordinates without having to recompile everything. 60 | 61 | ## Next 62 | 63 | Some ideas I have in mind to improve it : 64 | * Shortcuts to add some intervals to all notes 65 | * Shortcut to reset the intervals of one note 66 | * Different arpeggiator patterns 67 | 68 | ## Third party dependencies 69 | 70 | - [JUCE](https://juce.com/) 71 | - [Gin](https://github.com/FigBug/Gin) 72 | - [nlohmann/json](https://github.com/nlohmann/json) 73 | - [Iosevka font](https://github.com/be5invis/Iosevka) 74 | - [Scarlab Solid Oval Interface Icons Collection](https://www.svgrepo.com/collection/scarlab-solid-oval-interface-icons/) -------------------------------------------------------------------------------- /data/config/colors.json: -------------------------------------------------------------------------------- 1 | { 2 | "slider_background": "ff97cbf8", 3 | "slider_thumb": "ff013779", 4 | "slider_track": "ff2060a7", 5 | "slider_text": "ff121255", 6 | "slider_outline": "ffcfcfdf", 7 | "label_background": "ff153c69", 8 | "label_text": "ffeff1f1", 9 | "key_played_background": "ff42bbfc", 10 | "key_played_text": "ffeff1f1" 11 | } -------------------------------------------------------------------------------- /data/config/positions.json: -------------------------------------------------------------------------------- 1 | { 2 | "midi_panel": { 3 | "x": 0, "y": 0, "w": 342, "h": 172 4 | }, 5 | "midi_labels": { 6 | "x": 12, "y": 131, "w": 206, "h": 30 7 | }, 8 | "midi_oct": { 9 | "x": 228, "y": 131, "w": 100, "h": 30 10 | }, 11 | "arp_panel": { 12 | "x": 342, "y": 0, "w": 174, "h": 172 13 | }, 14 | "arp_switch": { 15 | "x": 14, "y": 132, "w": 60, "h": 28 16 | }, 17 | "arp_sync_switch": { 18 | "x": 82, "y": 132, "w": 80, "h": 28 19 | }, 20 | "presets_panel": { 21 | "x": 516, "y": 0, "w": 225, "h": 172 22 | }, 23 | "keys_panel": { 24 | "x": 0, "y": 172, "w": 741, "h": 93 25 | }, 26 | "intervals_panel": { 27 | "x": 0, "y": 265, "w": 741, "h": 135 28 | }, 29 | "intervals_transpose_label": { 30 | "x": 64, "y": 366, "w": 110, "h": 25 31 | }, 32 | "intervals_sliders_label": { 33 | "x": 180, "y": 366, "w": 540, "h": 25 34 | }, 35 | "tooltips_panel": { 36 | "x": 0, "y": 400, "w": 741, "h": 42 37 | }, 38 | "intervals_toggle": { 39 | "x": 8, "y": 40, "w": 60, "h": 30 40 | }, 41 | "margin": 4.0, 42 | "switch_margin": 3.0, 43 | "frame_corner": 6.0, 44 | "header_height": 48.0, 45 | "header_font_size": 30.0, 46 | "label_font_size": 28.0, 47 | "midi_in_x": 46.0, 48 | "midi_out_x": 152.0, 49 | "midi_oct_x": 256.0, 50 | "knob_height": 85.0, 51 | "toggle_height": 35.0, 52 | "toggle_width": 70.0, 53 | "toggle_sync_width": 90.0, 54 | "toggle_x": 3.0, 55 | "button_height": 40.0, 56 | "presets_ratio": 0.69, 57 | "key_font_size": 28.0, 58 | "key_corner": 16.0, 59 | "key_ratio": 1.9, 60 | "key_over": 1.03, 61 | "intervals_x": 65.0, 62 | "intervals_h": 110.0, 63 | "intervals_knob_w": 110.0, 64 | "intervals_sliders_w": 550.0, 65 | "intervals_label_corner": 12.0, 66 | "intervals_label_fontsize": 20.0 67 | } -------------------------------------------------------------------------------- /data/fonts/iosevka-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stfufane/Midi-Transposer/a6f56c0b5cccbdf3bf40ac2e383becd9ee1e44e4/data/fonts/iosevka-regular.ttf -------------------------------------------------------------------------------- /data/img/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /data/img/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/img/new.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/img/next.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/img/previous.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/img/save.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /get_cpm.cmake: -------------------------------------------------------------------------------- 1 | set(CPM_DOWNLOAD_VERSION 0.38.7) 2 | 3 | set(CPM_DOWNLOAD_LOCATION "${LIB_DIR}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") 4 | 5 | # Expand relative path. This is important if the provided path contains a tilde (~) 6 | get_filename_component(CPM_DOWNLOAD_LOCATION ${CPM_DOWNLOAD_LOCATION} ABSOLUTE) 7 | 8 | function(download_cpm) 9 | message(STATUS "Downloading CPM.cmake to ${CPM_DOWNLOAD_LOCATION}") 10 | file(DOWNLOAD 11 | https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake 12 | ${CPM_DOWNLOAD_LOCATION} 13 | ) 14 | endfunction() 15 | 16 | if(NOT (EXISTS ${CPM_DOWNLOAD_LOCATION})) 17 | download_cpm() 18 | else() 19 | # resume download if it previously failed 20 | file(READ ${CPM_DOWNLOAD_LOCATION} check) 21 | if("${check}" STREQUAL "") 22 | download_cpm() 23 | endif() 24 | unset(check) 25 | endif() 26 | 27 | include(${CPM_DOWNLOAD_LOCATION}) 28 | 29 | -------------------------------------------------------------------------------- /src/gui/CompColors.h: -------------------------------------------------------------------------------- 1 | #ifndef MIDITRANSPOSER_COMPCOLORS_H 2 | #define MIDITRANSPOSER_COMPCOLORS_H 3 | 4 | #include 5 | 6 | #include "nlohmann/json.hpp" 7 | 8 | namespace Gui 9 | { 10 | 11 | /** 12 | * @brief String representations of the implemented colors 13 | * that can be read from a json file. 14 | */ 15 | struct CompColors { 16 | std::string mSliderBackground; 17 | std::string mSliderThumb; 18 | std::string mSliderText; 19 | std::string mSliderTrack; 20 | std::string mSliderOutline; 21 | std::string mLabelBackground; 22 | std::string mLabelText; 23 | std::string mKeyPlayedBackground; 24 | std::string mKeyPlayedText; 25 | 26 | static std::string getFileName() { return "colors.json"; } 27 | }; 28 | 29 | inline void from_json(const nlohmann::json& j, CompColors& colors) 30 | { 31 | try { 32 | j.at("slider_background").get_to(colors.mSliderBackground); 33 | j.at("slider_thumb").get_to(colors.mSliderThumb); 34 | j.at("slider_text").get_to(colors.mSliderText); 35 | j.at("slider_track").get_to(colors.mSliderTrack); 36 | j.at("slider_outline").get_to(colors.mSliderOutline); 37 | j.at("label_background").get_to(colors.mLabelBackground); 38 | j.at("label_text").get_to(colors.mLabelText); 39 | j.at("key_played_background").get_to(colors.mKeyPlayedBackground); 40 | j.at("key_played_text").get_to(colors.mKeyPlayedText); 41 | } catch (std::exception& e) { 42 | std::cout << "One or several values were not defined in the json configuration file\n" << e.what() << "\n"; 43 | } 44 | } 45 | 46 | } 47 | 48 | #endif -------------------------------------------------------------------------------- /src/gui/CompCoordinates.h: -------------------------------------------------------------------------------- 1 | #ifndef MIDITRANSPOSER_COMPCOORDINATES_H 2 | #define MIDITRANSPOSER_COMPCOORDINATES_H 3 | 4 | #include "nlohmann/json.hpp" 5 | 6 | namespace Gui 7 | { 8 | 9 | struct CompCoordinates { 10 | // Coordinates of the different panels. 11 | juce::Rectangle mMidiPanel; 12 | juce::Rectangle mMidiLabels; 13 | juce::Rectangle mMidiOct; 14 | juce::Rectangle mArpPanel; 15 | juce::Rectangle mArpSwitch; 16 | juce::Rectangle mArpSyncSwitch; 17 | juce::Rectangle mPresetsPanel; 18 | juce::Rectangle mKeysPanel; 19 | juce::Rectangle mIntervalsPanel; 20 | juce::Rectangle mIntervalsTransposeLabel; 21 | juce::Rectangle mIntervalsSlidersLabel; 22 | juce::Rectangle mTooltipsPanel; 23 | juce::Rectangle mIntervalsToggle; 24 | 25 | // Some globals to draw different components. 26 | float mMargin { 1.f }; // The global margin separating the different panels 27 | float mSwitchMargin { 1.f }; 28 | float mFrameCorner { 1.f }; // The frame's rounded rectangles size 29 | float mHeaderHeight { 1.f }; // The height of the title blocks 30 | float mHeaderFontSize { 1.f }; // The font size of the title blocks 31 | float mLabelFontSize { 1.f }; // The font size of the block labels 32 | float mMidiInX { 1.f }; 33 | float mMidiOutX { 1.f }; 34 | float mMidiOctX { 1.f }; 35 | float mKnobHeight { 1.f }; 36 | float mToggleHeight { 1.f }; 37 | float mToggleWidth { 1.f }; 38 | float mToggleSyncWidth { 1.f }; 39 | float mToggleX { 1.f }; 40 | float mButtonHeight { 1.f }; 41 | float mKeyFontSize { 1.f }; 42 | float mKeyCorner { 1.f }; 43 | float mKeyRatio { 1.f }; 44 | float mKeyOver { 1.f }; 45 | float mIntervalsX { 1.f }; 46 | float mIntervalsH { 1.f }; 47 | float mIntervalKnobW { 1.f }; 48 | float mIntervalsSlidersW { 1.f }; 49 | float mIntervalsLabelCorner { 1.f }; 50 | float mIntervalsLabelFontSize { 1.f }; 51 | 52 | static std::string getFileName() { return "positions.json"; } 53 | }; 54 | 55 | inline void from_json(const nlohmann::json& j, CompCoordinates& pos) 56 | { 57 | auto midi_panel = j.at("midi_panel"); 58 | pos.mMidiPanel = { midi_panel.at("x"), midi_panel.at("y"), 59 | midi_panel.at("w"), midi_panel.at("h") }; 60 | auto midi_labels = j.at("midi_labels"); 61 | pos.mMidiLabels = { midi_labels.at("x"), midi_labels.at("y"), 62 | midi_labels.at("w"), midi_labels.at("h") }; 63 | auto midi_oct = j.at("midi_oct"); 64 | pos.mMidiOct = { midi_oct.at("x"), midi_oct.at("y"), 65 | midi_oct.at("w"), midi_oct.at("h") }; 66 | auto arp_panel = j.at("arp_panel"); 67 | pos.mArpPanel = { arp_panel.at("x"), arp_panel.at("y"), 68 | arp_panel.at("w"), arp_panel.at("h") }; 69 | auto arp_switch = j.at("arp_switch"); 70 | pos.mArpSwitch = { arp_switch.at("x"), arp_switch.at("y"), 71 | arp_switch.at("w"), arp_switch.at("h") }; 72 | auto arp_sync_switch = j.at("arp_sync_switch"); 73 | pos.mArpSyncSwitch = { arp_sync_switch.at("x"), arp_sync_switch.at("y"), 74 | arp_sync_switch.at("w"), arp_sync_switch.at("h") }; 75 | auto presets_panel = j.at("presets_panel"); 76 | pos.mPresetsPanel = { presets_panel.at("x"), presets_panel.at("y"), 77 | presets_panel.at("w"), presets_panel.at("h") }; 78 | auto keys_panel = j.at("keys_panel"); 79 | pos.mKeysPanel = { keys_panel.at("x"), keys_panel.at("y"), 80 | keys_panel.at("w"), keys_panel.at("h") }; 81 | auto intervals_panel = j.at("intervals_panel"); 82 | pos.mIntervalsPanel = { intervals_panel.at("x"), intervals_panel.at("y"), 83 | intervals_panel.at("w"), intervals_panel.at("h") }; 84 | auto intervals_transpose_label = j.at("intervals_transpose_label"); 85 | pos.mIntervalsTransposeLabel = { intervals_transpose_label.at("x"), intervals_transpose_label.at("y"), 86 | intervals_transpose_label.at("w"), intervals_transpose_label.at("h") }; 87 | auto intervals_sliders_label = j.at("intervals_sliders_label"); 88 | pos.mIntervalsSlidersLabel = { intervals_sliders_label.at("x"), intervals_sliders_label.at("y"), 89 | intervals_sliders_label.at("w"), intervals_sliders_label.at("h") }; 90 | auto tooltips_panel = j.at("tooltips_panel"); 91 | pos.mTooltipsPanel = { tooltips_panel.at("x"), tooltips_panel.at("y"), 92 | tooltips_panel.at("w"), tooltips_panel.at("h") }; 93 | auto intervals_toggle = j.at("intervals_toggle"); 94 | pos.mIntervalsToggle = { intervals_toggle.at("x"), intervals_toggle.at("y"), 95 | intervals_toggle.at("w"), intervals_toggle.at("h") }; 96 | 97 | j.at("margin").get_to(pos.mMargin); 98 | j.at("switch_margin").get_to(pos.mSwitchMargin); 99 | j.at("frame_corner").get_to(pos.mFrameCorner); 100 | j.at("header_height").get_to(pos.mHeaderHeight); 101 | j.at("header_font_size").get_to(pos.mHeaderFontSize); 102 | j.at("label_font_size").get_to(pos.mLabelFontSize); 103 | j.at("midi_in_x").get_to(pos.mMidiInX); 104 | j.at("midi_out_x").get_to(pos.mMidiOutX); 105 | j.at("midi_oct_x").get_to(pos.mMidiOctX); 106 | j.at("knob_height").get_to(pos.mKnobHeight); 107 | j.at("toggle_height").get_to(pos.mToggleHeight); 108 | j.at("toggle_width").get_to(pos.mToggleWidth); 109 | j.at("toggle_sync_width").get_to(pos.mToggleSyncWidth); 110 | j.at("toggle_x").get_to(pos.mToggleX); 111 | j.at("button_height").get_to(pos.mButtonHeight); 112 | j.at("key_font_size").get_to(pos.mKeyFontSize); 113 | j.at("key_corner").get_to(pos.mKeyCorner); 114 | j.at("key_ratio").get_to(pos.mKeyRatio); 115 | j.at("key_over").get_to(pos.mKeyOver); 116 | j.at("intervals_x").get_to(pos.mIntervalsX); 117 | j.at("intervals_h").get_to(pos.mIntervalsH); 118 | j.at("intervals_knob_w").get_to(pos.mIntervalKnobW); 119 | j.at("intervals_sliders_w").get_to(pos.mIntervalsSlidersW); 120 | j.at("intervals_label_corner").get_to(pos.mIntervalsLabelCorner); 121 | j.at("intervals_label_fontsize").get_to(pos.mIntervalsLabelFontSize); 122 | } 123 | 124 | } 125 | 126 | #endif //MIDITRANSPOSER_COMPCOORDINATES_H 127 | -------------------------------------------------------------------------------- /src/gui/Configuration.hpp: -------------------------------------------------------------------------------- 1 | #ifndef MIDITRANSPOSER_CONFIGURATION_HPP 2 | #define MIDITRANSPOSER_CONFIGURATION_HPP 3 | 4 | #include 5 | #include "nlohmann/json.hpp" 6 | #include "gin/gin.h" 7 | 8 | namespace Gui 9 | { 10 | /** 11 | * @brief A helper class to listen to changes on a certain configuration file and trigger actions when it's modified 12 | * @tparam Data a struct type that can be serialized from json. 13 | * It also has to implement a getFileName static method that returns a std::string 14 | */ 15 | template 16 | class Configuration final : public gin::FileSystemWatcher::Listener 17 | { 18 | public: 19 | Configuration() = delete; 20 | 21 | explicit Configuration(std::string watchedFolder, juce::Component* rootComponent) : 22 | mWatchedFolder(std::move(watchedFolder)), mRootComponent(rootComponent) 23 | { 24 | #if _DEBUG 25 | // Do not start watching folders in release mode 26 | mFileSystemWatcher.addFolder(juce::File(mWatchedFolder)); 27 | #endif 28 | mFileSystemWatcher.addListener(this); 29 | 30 | reloadConfiguration(); 31 | } 32 | 33 | ~Configuration() override 34 | { 35 | mFileSystemWatcher.removeListener(this); 36 | } 37 | 38 | /** 39 | * @brief Declare a simple listener type for classes that use the dynamic config 40 | */ 41 | template 42 | class Listener { 43 | public: 44 | virtual ~Listener() = default; 45 | 46 | /** 47 | * @brief Send the updated data 48 | */ 49 | virtual void onConfigChanged(const T&) {} 50 | }; 51 | 52 | void addListener(Listener* inListener) { mListeners.insert(inListener); } 53 | void removeListener(Listener* inListener) { mListeners.erase(inListener); } 54 | 55 | /** 56 | * @brief Triggered by the file-watcher when a file in the listened folder has been modified 57 | */ 58 | void fileChanged(const juce::File& inFile, gin::FileSystemWatcher::FileSystemEvent inEvent) override 59 | { 60 | // Only interested in modified files 61 | if (inEvent != gin::FileSystemWatcher::FileSystemEvent::fileUpdated) { 62 | return; 63 | } 64 | 65 | // Check if the right file is modified 66 | if (inFile.getFileName().toStdString() != Data::getFileName()) { 67 | return; 68 | } 69 | 70 | if (reloadConfiguration()) { 71 | notifyListeners(); 72 | if (mRootComponent) { 73 | juce::MessageManager::callAsync([&]() { mRootComponent->repaint(); }); 74 | } 75 | } 76 | } 77 | 78 | [[nodiscard]] const Data& getData() const { return mData; } 79 | 80 | private: 81 | bool reloadConfiguration() 82 | { 83 | // Read json config 84 | const std::string json_path = mWatchedFolder + "/" + Data::getFileName(); 85 | try { 86 | std::ifstream f(json_path); 87 | auto j = nlohmann::json::parse(f); 88 | mData = j; 89 | return true; 90 | } catch (std::exception& e) { 91 | std::cout << "Failed to read/parse the json file " << json_path << ", error : " << e.what() << "\n\n"; 92 | return false; 93 | } 94 | } 95 | 96 | void notifyListeners() 97 | { 98 | for (auto* listener: mListeners) { 99 | listener->onConfigChanged(mData); 100 | } 101 | } 102 | 103 | /** 104 | * @brief A background thread that will listen to changes on the wanted file 105 | */ 106 | gin::FileSystemWatcher mFileSystemWatcher; 107 | 108 | /** 109 | * @brief The folder that contains the file to watch. 110 | */ 111 | std::string mWatchedFolder; 112 | 113 | /** 114 | * @brief Reference to the component holding the configuration, necessary to trigger repaint after a config change. 115 | */ 116 | juce::Component* mRootComponent = nullptr; 117 | 118 | /** 119 | * @brief Keep track of the listeners to notify when changes have been made 120 | */ 121 | std::set*> mListeners; 122 | 123 | /** 124 | * @brief The type of data that will be retrieved from json 125 | */ 126 | Data mData; 127 | }; 128 | 129 | } 130 | 131 | #endif //MIDITRANSPOSER_CONFIGURATION_HPP 132 | -------------------------------------------------------------------------------- /src/gui/PluginEditor.cpp: -------------------------------------------------------------------------------- 1 | #include "PluginEditor.h" 2 | 3 | //============================================================================== 4 | MidiTransposerAudioProcessorEditor::MidiTransposerAudioProcessorEditor(MidiTransposerAudioProcessor& p) 5 | : juce::AudioProcessorEditor(&p), 6 | mLookAndFeel(this), 7 | mainPanel(p), 8 | tooltipWindow(mainPanel.getTooltipPanel(), 50) 9 | { 10 | setResizable(true, true); 11 | 12 | // Retrieve useful data from the processor. 13 | const auto& uiSettings = p.getUISettings(); 14 | 15 | // Restore the last size if it exists. 16 | if (uiSettings.width != 0) { 17 | setSize(uiSettings.width, uiSettings.height); 18 | } else { 19 | setSize(kWindowWidth, kWindowHeight); 20 | } 21 | setResizeLimits(kWindowWidth, kWindowHeight, 22 | static_cast(kWindowWidth * kMaxResize), static_cast(kWindowHeight * kMaxResize)); 23 | getConstrainer()->setFixedAspectRatio(kWindowRatio); 24 | 25 | setLookAndFeel(&mLookAndFeel); 26 | tooltipWindow.setLookAndFeel(&mLookAndFeel); 27 | 28 | addAndMakeVisible(mainPanel); 29 | } 30 | 31 | MidiTransposerAudioProcessorEditor::~MidiTransposerAudioProcessorEditor() 32 | { 33 | setLookAndFeel(nullptr); 34 | } 35 | 36 | //============================================================================== 37 | void MidiTransposerAudioProcessorEditor::paint(juce::Graphics& g) 38 | { 39 | g.fillAll(juce::Colours::whitesmoke); 40 | } 41 | 42 | void MidiTransposerAudioProcessorEditor::resized() 43 | { 44 | auto* p = dynamic_cast(&processor); 45 | p->saveEditorSize(getWidth(), getHeight()); 46 | 47 | mainPanel.setSize(kWindowWidth, kWindowHeight); 48 | mainPanel.setTransform(juce::AffineTransform::scale(static_cast(getWidth()) / static_cast(kWindowWidth))); 49 | } 50 | -------------------------------------------------------------------------------- /src/gui/PluginEditor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "processor/PluginProcessor.h" 4 | #include "gui/lookandfeel/BaseLookAndFeel.h" 5 | #include "gui/panels/MainPanel.h" 6 | 7 | class MidiTransposerAudioProcessorEditor final : public juce::AudioProcessorEditor 8 | { 9 | public: 10 | explicit MidiTransposerAudioProcessorEditor(MidiTransposerAudioProcessor& p); 11 | 12 | ~MidiTransposerAudioProcessorEditor() override; 13 | 14 | //============================================================================== 15 | void paint(juce::Graphics&) override; 16 | 17 | void resized() override; 18 | 19 | private: 20 | /** 21 | * @brief The main LookAndFeel that will be applied to all the children components 22 | */ 23 | Gui::LnF::BaseLookAndFeel mLookAndFeel; 24 | 25 | /** 26 | * @brief Arranges the different sections of the plugin in one place. 27 | */ 28 | Gui::MainPanel mainPanel; 29 | 30 | juce::TooltipWindow tooltipWindow; 31 | 32 | static constexpr float kWindowRatio = 1.676f; 33 | static constexpr int kWindowWidth = 741; 34 | static constexpr int kWindowHeight = 442; 35 | static constexpr float kMaxResize = 1.5f; 36 | 37 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MidiTransposerAudioProcessorEditor) 38 | }; -------------------------------------------------------------------------------- /src/gui/UISettings.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace Gui 4 | { 5 | /** 6 | * @brief A representation of the last UI settings to restore it when loading the editor 7 | */ 8 | struct UISettings 9 | { 10 | UISettings() = default; 11 | 12 | // Get it from the plugin state 13 | explicit UISettings(const juce::XmlElement* xml) 14 | { 15 | if (xml != nullptr) { 16 | width = xml->getIntAttribute("width"); 17 | height = xml->getIntAttribute("height"); 18 | lastNoteIndex = xml->getIntAttribute("lastNoteIndex"); 19 | presetName = xml->getStringAttribute("presetName"); 20 | } 21 | } 22 | 23 | // Build the XML representation to save in plugin state. 24 | [[nodiscard]] juce::XmlElement* getXml() const 25 | { 26 | auto* xml = new juce::XmlElement("UISettings"); 27 | xml->setAttribute("width", width); 28 | xml->setAttribute("height", height); 29 | xml->setAttribute("lastNoteIndex", lastNoteIndex); 30 | xml->setAttribute("presetName", presetName); 31 | return xml; 32 | } 33 | 34 | int width = 0; 35 | int height = 0; 36 | int lastNoteIndex = 0; 37 | juce::String presetName; 38 | }; 39 | 40 | } -------------------------------------------------------------------------------- /src/gui/lookandfeel/BaseLookAndFeel.cpp: -------------------------------------------------------------------------------- 1 | #include "BaseLookAndFeel.h" 2 | 3 | namespace Gui::LnF 4 | { 5 | 6 | BaseLookAndFeel::BaseLookAndFeel(juce::Component* rootComponent) 7 | : mConfiguration(CONFIG_FOLDER, rootComponent) 8 | { 9 | mConfiguration.addListener(this); 10 | resetColors(); 11 | } 12 | 13 | BaseLookAndFeel::~BaseLookAndFeel() 14 | { 15 | mConfiguration.removeListener(this); 16 | } 17 | 18 | void BaseLookAndFeel::onConfigChanged(const CompColors&) 19 | { 20 | resetColors(); 21 | } 22 | 23 | void BaseLookAndFeel::resetColors() { 24 | const auto& colors = mConfiguration.getData(); 25 | // Slider colors 26 | setColour(juce::Slider::backgroundColourId, juce::Colour::fromString(colors.mSliderBackground)); 27 | setColour(juce::Slider::thumbColourId, juce::Colour::fromString(colors.mSliderThumb)); 28 | setColour(juce::Slider::textBoxTextColourId, juce::Colour::fromString(colors.mSliderText)); 29 | setColour(juce::Slider::trackColourId, juce::Colour::fromString(colors.mSliderTrack)); 30 | setColour(juce::Slider::rotarySliderOutlineColourId, juce::Colour::fromString(colors.mSliderOutline)); 31 | 32 | // Label colors 33 | setColour(juce::Label::ColourIds::textColourId, juce::Colour::fromString(colors.mLabelText)); 34 | setColour(juce::Label::ColourIds::backgroundColourId, juce::Colour::fromString(colors.mLabelBackground)); 35 | 36 | // Tooltip colors 37 | setColour(juce::TooltipWindow::backgroundColourId, juce::Colours::whitesmoke); 38 | setColour(juce::TooltipWindow::textColourId, juce::Colours::black); 39 | 40 | setColour(juce::TextButton::ColourIds::buttonColourId, juce::Colour::fromString(colors.mKeyPlayedBackground)); 41 | setColour(juce::TextButton::ColourIds::textColourOnId, juce::Colour::fromString(colors.mKeyPlayedText)); 42 | 43 | // ToggleButton colors 44 | setColour(juce::ToggleButton::ColourIds::textColourId, juce::Colours::black); 45 | setColour(juce::ToggleButton::ColourIds::tickColourId, juce::Colours::black); 46 | setColour(juce::ToggleButton::ColourIds::tickDisabledColourId, juce::Colours::darkgrey); 47 | 48 | setColour(juce::PopupMenu::ColourIds::backgroundColourId, juce::Colours::transparentWhite); 49 | } 50 | 51 | juce::Rectangle BaseLookAndFeel::getTooltipBounds(const juce::String& /* tipText */, 52 | juce::Point /* screenPos */, 53 | const juce::Rectangle parentArea) 54 | { 55 | // Return the global bounds of the tooltips, at the bottom of the plugin area. 56 | return juce::Rectangle(0, 0, parentArea.getWidth(), parentArea.getHeight()).reduced(3, 1); 57 | } 58 | 59 | void BaseLookAndFeel::drawTooltip(juce::Graphics& g, const juce::String& text, int width, int height) 60 | { 61 | juce::Rectangle bounds(width, height); 62 | // Fill the background 63 | g.setColour(findColour(juce::TooltipWindow::backgroundColourId)); 64 | g.fillRect(bounds.toFloat()); 65 | 66 | // Draw the text 67 | g.setColour(findColour(juce::Label::backgroundColourId)); 68 | g.setFont(getDefaultFont()); 69 | g.drawText(text, bounds, juce::Justification::centredLeft); 70 | } 71 | 72 | /** 73 | * @brief Draw a simple rectangular slider with a square thumb. Handling only vertical because I'm lazy and only use this one. 74 | */ 75 | void BaseLookAndFeel::drawLinearSlider(juce::Graphics& g, int x, int y, int width, int height, float sliderPos, 76 | float /* minSliderPos */, float /* maxSliderPos */, const juce::Slider::SliderStyle, 77 | juce::Slider& slider) 78 | { 79 | auto trackWidth = 10.f; 80 | constexpr auto h_ratio = .72f; 81 | 82 | const juce::Point startPoint (static_cast(x) + static_cast(width) * 0.5f, static_cast(height + y) * h_ratio); 83 | const juce::Point endPoint (startPoint.x, static_cast(y) * h_ratio); 84 | 85 | juce::Path backgroundTrack; 86 | backgroundTrack.startNewSubPath (startPoint); 87 | backgroundTrack.lineTo (endPoint); 88 | g.setColour (slider.findColour (juce::Slider::backgroundColourId)); 89 | g.strokePath (backgroundTrack, { trackWidth, juce::PathStrokeType::mitered, juce::PathStrokeType::square }); 90 | 91 | juce::Path valueTrack; 92 | 93 | const auto kx = static_cast(x) + static_cast(width) * 0.5f; 94 | const auto ky = sliderPos * h_ratio; 95 | 96 | const juce::Point minPoint = startPoint; 97 | const juce::Point maxPoint = { kx, ky }; 98 | 99 | valueTrack.startNewSubPath (minPoint); 100 | valueTrack.lineTo (maxPoint); 101 | g.setColour (slider.findColour (juce::Slider::backgroundColourId)); 102 | g.strokePath (valueTrack, { trackWidth, juce::PathStrokeType::mitered, juce::PathStrokeType::square }); 103 | 104 | g.setColour (slider.findColour (juce::Slider::thumbColourId)); 105 | g.fillRect(juce::Rectangle (trackWidth * 2.f, trackWidth / 2.f).withCentre (maxPoint)); 106 | 107 | const auto value = slider.getValue(); 108 | const auto text = (value > 0 ? "+" : "") + juce::String(value); 109 | const auto text_bounds = juce::Rectangle(0.f, static_cast(slider.getHeight()) * h_ratio, 110 | static_cast(width), static_cast(slider.getHeight()) * (1.f - h_ratio)); 111 | 112 | g.setFont(LnF::getDefaultFont(20.f)); 113 | g.setColour(findColour(juce::Label::ColourIds::backgroundColourId)); 114 | g.drawText(text, text_bounds, juce::Justification::centred); 115 | } 116 | 117 | void BaseLookAndFeel::drawRotarySlider (juce::Graphics& g, int /* x */, const int y, const int width, const int height, 118 | const float sliderPos, const float rotaryStartAngle, 119 | const float rotaryEndAngle, juce::Slider& slider) 120 | { 121 | const auto outline = slider.findColour (juce::Slider::backgroundColourId); 122 | const auto fill = slider.findColour (juce::Slider::trackColourId); 123 | 124 | const auto bounds = juce::Rectangle ((width - height) / 2, y, // TODO: calculate properly 125 | juce::jmin(width, height), juce::jmin(width, height)).toFloat().reduced (5); 126 | 127 | const auto radius = juce::jmin (bounds.getWidth(), bounds.getHeight()) / 2.0f; 128 | const auto toAngle = rotaryStartAngle + sliderPos * (rotaryEndAngle - rotaryStartAngle); 129 | constexpr auto lineW = 2.0f; 130 | const auto arcRadius = radius - lineW * 0.5f + 2.0f; 131 | 132 | g.setColour (outline); 133 | g.fillEllipse(bounds.reduced(3.0f)); 134 | 135 | // First draw the outline 136 | juce::Path backgroundArc; 137 | backgroundArc.addCentredArc (bounds.getCentreX(), 138 | bounds.getCentreY(), 139 | arcRadius, 140 | arcRadius, 141 | 0.0f, 142 | toAngle, 143 | rotaryEndAngle, 144 | true); 145 | 146 | g.setColour(slider.findColour(juce::Slider::rotarySliderOutlineColourId)); 147 | g.strokePath(backgroundArc, juce::PathStrokeType (lineW, juce::PathStrokeType::mitered, juce::PathStrokeType::square)); 148 | 149 | // Then the value 150 | juce::Path valueArc; 151 | valueArc.addCentredArc (bounds.getCentreX(), 152 | bounds.getCentreY(), 153 | arcRadius, 154 | arcRadius, 155 | 0.0f, 156 | rotaryStartAngle, 157 | toAngle, 158 | true); 159 | 160 | g.setColour (fill); 161 | g.strokePath (valueArc, juce::PathStrokeType (lineW, juce::PathStrokeType::mitered, juce::PathStrokeType::square)); 162 | 163 | // The thumb is just a small square 164 | constexpr auto thumbWidth = lineW * 2.f; 165 | const juce::Point thumbPoint (bounds.getCentreX() + arcRadius * std::cos (toAngle - juce::MathConstants::halfPi), 166 | bounds.getCentreY() + arcRadius * std::sin (toAngle - juce::MathConstants::halfPi)); 167 | 168 | g.setColour (slider.findColour (juce::Slider::thumbColourId)); 169 | g.fillRect(juce::Rectangle (thumbWidth, thumbWidth).withCentre (thumbPoint)); 170 | } 171 | 172 | void BaseLookAndFeel::drawComboBox (juce::Graphics& g, const int width, const int height, bool /* isButtonDown */, 173 | int /* buttonX */, int /* buttonY */, int /* buttonW */, int /* buttonH */, 174 | juce::ComboBox& box) 175 | { 176 | constexpr auto cornerSize = 16.f; 177 | const auto bounds = juce::Rectangle (0, 0, width, height).toFloat().reduced (3.f, 0.f); 178 | 179 | g.setColour (box.findColour (juce::Label::backgroundColourId).darker(box.isMouseOver(true) ? .2f : .1f)); 180 | g.fillRoundedRectangle (bounds, cornerSize); 181 | } 182 | 183 | void BaseLookAndFeel::positionComboBoxText (juce::ComboBox& box, juce::Label& label) 184 | { 185 | label.setBounds (juce::Rectangle(0, 0, box.getWidth(), box.getHeight()).reduced(3, 1)); 186 | label.setFont (LnF::getDefaultFont(18.f)); 187 | label.setJustificationType(juce::Justification::centred); 188 | } 189 | 190 | void BaseLookAndFeel::drawPopupMenuBackground (juce::Graphics& g, int width, int height) 191 | { 192 | g.fillAll(juce::Colours::transparentWhite); 193 | 194 | g.setColour(findColour(juce::Label::backgroundColourId)); 195 | g.fillRect(juce::Rectangle(0, 0, width - 30, height).withCentre(juce::Point(width / 2, height / 2))); 196 | } 197 | 198 | void BaseLookAndFeel::drawPopupMenuItem (juce::Graphics& g, const juce::Rectangle& area, 199 | const bool /* isSeparator */, const bool isActive, 200 | const bool isHighlighted, const bool /* isTicked */, 201 | const bool /* hasSubMenu */, const juce::String& text, 202 | const juce::String& /* shortcutKeyText */, 203 | const juce::Drawable* /* icon */, const juce::Colour* const /* textColourToUse */) 204 | { 205 | g.fillAll(juce::Colours::transparentWhite); 206 | 207 | const auto textColour = findColour (juce::Label::textColourId); 208 | auto r = area.reduced (1); 209 | r.reduce (juce::jmin (5, area.getWidth() / 20), 0); 210 | 211 | if (isHighlighted && isActive) { 212 | g.setColour (findColour (juce::Label::backgroundColourId).brighter(0.2f)); 213 | g.fillRect (r); 214 | 215 | g.setColour (findColour (juce::PopupMenu::highlightedTextColourId)); 216 | } else { 217 | g.setColour (textColour.withMultipliedAlpha (isActive ? 1.0f : 0.5f)); 218 | } 219 | 220 | auto font = getDefaultFont(); 221 | const auto maxFontHeight = static_cast(r.getHeight()) / 1.3f; 222 | if (font.getHeight() > maxFontHeight) 223 | font.setHeight (maxFontHeight); 224 | 225 | g.setFont (font); 226 | g.drawFittedText (text, r, juce::Justification::centred, 1); 227 | 228 | } 229 | 230 | } -------------------------------------------------------------------------------- /src/gui/lookandfeel/BaseLookAndFeel.h: -------------------------------------------------------------------------------- 1 | #ifndef MIDITRANSPOSER_BASELOOKANDFEEL_H 2 | #define MIDITRANSPOSER_BASELOOKANDFEEL_H 3 | 4 | #include 5 | #include "BinaryData.h" 6 | #include "gui/Configuration.hpp" 7 | #include "gui/CompColors.h" 8 | 9 | namespace Gui::LnF 10 | { 11 | 12 | inline juce::Font getDefaultFont(const float inPointHeight = 16.f) 13 | { 14 | return juce::Font(juce::Typeface::createSystemTypefaceFor(BinaryData::iosevkaregular_ttf, 15 | BinaryData::iosevkaregular_ttfSize)) 16 | .withPointHeight(inPointHeight); 17 | } 18 | 19 | class BaseLookAndFeel final : public juce::LookAndFeel_V4, 20 | public Gui::Configuration::Listener 21 | { 22 | public: 23 | explicit BaseLookAndFeel(juce::Component* rootComponent); 24 | ~BaseLookAndFeel() override; 25 | 26 | juce::Rectangle getTooltipBounds(const juce::String& tipText, 27 | juce::Point screenPos, 28 | juce::Rectangle parentArea) override; 29 | 30 | void drawTooltip(juce::Graphics& g, const juce::String& text, int width, int height) override; 31 | 32 | void drawLinearSlider(juce::Graphics&, int x, int y, int width, int height, 33 | float sliderPos, float minSliderPos, float maxSliderPos, 34 | juce::Slider::SliderStyle, juce::Slider&) override; 35 | 36 | void drawRotarySlider (juce::Graphics&, int x, int y, int width, int height, 37 | float sliderPos, float rotaryStartAngle, 38 | float rotaryEndAngle, juce::Slider&) override; 39 | 40 | void drawComboBox(juce::Graphics& g, int width, int height, bool isButtonDown, 41 | int buttonX, int buttonY, int buttonW, int buttonH, 42 | juce::ComboBox& box) override; 43 | 44 | void positionComboBoxText(juce::ComboBox& box, juce::Label& label) override; 45 | 46 | void onConfigChanged(const CompColors& colors) override; 47 | 48 | void drawPopupMenuBackground (juce::Graphics& g, int width, int height) override; 49 | 50 | void drawPopupMenuItem (juce::Graphics& g, const juce::Rectangle& area, 51 | bool isSeparator, bool isActive, 52 | bool isHighlighted, bool isTicked, 53 | bool hasSubMenu, const juce::String& text, 54 | const juce::String& shortcutKeyText, 55 | const juce::Drawable* icon, const juce::Colour* textColourToUse) override; 56 | 57 | private: 58 | Gui::Configuration mConfiguration; 59 | 60 | void resetColors(); 61 | }; 62 | 63 | } 64 | #endif //MIDITRANSPOSER_BASELOOKANDFEEL_H 65 | -------------------------------------------------------------------------------- /src/gui/panels/ArpPanel.cpp: -------------------------------------------------------------------------------- 1 | #include "ArpPanel.h" 2 | #include "gui/panels/MainPanel.h" 3 | #include "gui/lookandfeel/BaseLookAndFeel.h" 4 | 5 | namespace Gui 6 | { 7 | 8 | ArpPanel::ArpPanel(MidiTransposerAudioProcessor& p) 9 | : juce::Component("Arp Panel") 10 | { 11 | const auto& arpParams = p.getMidiProcessor().getArpeggiatorParams(); 12 | 13 | arpActivated = std::make_unique< AttachedComponent >( 14 | *arpParams.activated, *this, 15 | [](Gui::TextSwitch& button) { 16 | button.setCustomTooltipLambda([&button]() -> juce::String { 17 | return juce::String(button.getToggleState() ? "Dea" : "A") + "ctivate the arpeggiator"; 18 | }); 19 | }, 20 | "Arp Activation Toggle", "ON", "OFF", 14.f 21 | ); 22 | 23 | arpRate = std::make_unique< AttachedComponent >( 24 | *arpParams.rate, *this, 25 | [](Gui::CustomSlider& slider) { 26 | slider.setRange(0., 1.0, 0.01); 27 | slider.setNumDecimalPlacesToDisplay(0); 28 | slider.setCustomTextLambda([](const double value) -> juce::String { 29 | return juce::String(1000. / (100. * (.1 + (5. - 5. * value))), 1) + "Hz"; 30 | }); 31 | slider.setCustomPaintLambda([&slider](juce::Graphics& g) { 32 | auto text = slider.getTextFromValue(slider.getValue()); 33 | auto bounds = slider.getLocalBounds(); 34 | g.setFont(LnF::getDefaultFont(18.f)); 35 | g.drawText(text, bounds, juce::Justification::centred); 36 | }); 37 | }, 38 | "Arp Rate Slider", juce::Slider::SliderStyle::RotaryVerticalDrag 39 | ); 40 | 41 | arpSyncRate = std::make_unique< AttachedComponent >( 42 | *arpParams.syncRate, *this, 43 | [](Gui::CustomSlider& slider) { 44 | double nbDivisions = static_cast(Notes::divisions.size()) - 1; 45 | slider.setNormalisableRange({ 0, nbDivisions, 1 }); 46 | slider.setCustomTextLambda([](const double value) -> juce::String { 47 | return Notes::divisions[static_cast(value)].label; 48 | }); 49 | slider.setCustomPaintLambda([&slider](juce::Graphics& g) { 50 | auto text = slider.getTextFromValue(slider.getValue()); 51 | auto bounds = slider.getLocalBounds(); 52 | g.setFont(LnF::getDefaultFont(18.f)); 53 | g.drawText(text, bounds, juce::Justification::centred); 54 | }); 55 | }, 56 | "Arp Sync Rate Slider", juce::Slider::SliderStyle::RotaryVerticalDrag 57 | ); 58 | 59 | arpSynced = std::make_unique< AttachedComponent >( 60 | *arpParams.synced, *this, 61 | [this](Gui::TextSwitch& button) { 62 | button.onStateChange = [this]() { 63 | resized(); 64 | }; 65 | button.setCustomTooltipLambda([&button]() -> juce::String { 66 | return !button.getToggleState() ? "Set the arpeggiator rate to tempo sync" : "Set the arpeggiator rate to an arbitrary value"; 67 | }); 68 | }, 69 | "Arp Sync Toggle", "SYNC", "FREE", 14.f 70 | ); 71 | } 72 | 73 | void ArpPanel::resized() 74 | { 75 | arpRate->getComponent().setVisible(!arpSynced->getComponent().getToggleState()); 76 | arpSyncRate->getComponent().setVisible(arpSynced->getComponent().getToggleState()); 77 | 78 | if (auto* main_panel = findParentComponentOfClass(); main_panel) { 79 | const auto& coordinates = main_panel->getCoordinates(); 80 | auto knob_bounds = juce::Rectangle(0, coordinates.mHeaderHeight, 81 | static_cast(getWidth()), coordinates.mKnobHeight) 82 | .reduced(coordinates.mMargin).toNearestInt(); 83 | arpRate->getComponent().setBounds(knob_bounds); 84 | arpSyncRate->getComponent().setBounds(knob_bounds); 85 | 86 | arpActivated->getComponent().setBounds(coordinates.mArpSwitch.toNearestInt()); 87 | arpSynced->getComponent().setBounds(coordinates.mArpSyncSwitch.toNearestInt()); 88 | } 89 | } 90 | 91 | } -------------------------------------------------------------------------------- /src/gui/panels/ArpPanel.h: -------------------------------------------------------------------------------- 1 | #ifndef MIDITRANSPOSER_ARPPANEL_H 2 | #define MIDITRANSPOSER_ARPPANEL_H 3 | 4 | #include "processor/PluginProcessor.h" 5 | #include "gui/widgets/Helpers.h" 6 | #include "gui/widgets/CustomSlider.h" 7 | #include "gui/widgets/TextSwitch.h" 8 | 9 | namespace Gui 10 | { 11 | 12 | class ArpPanel : public juce::Component 13 | { 14 | public: 15 | ArpPanel() = delete; 16 | explicit ArpPanel(MidiTransposerAudioProcessor& p); 17 | 18 | void resized() override; 19 | private: 20 | std::unique_ptr< AttachedComponent > arpActivated; 21 | std::unique_ptr< AttachedComponent > arpSynced; 22 | std::unique_ptr< AttachedComponent > arpSyncRate; 23 | std::unique_ptr< AttachedComponent > arpRate; 24 | 25 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ArpPanel) 26 | }; 27 | 28 | } 29 | 30 | #endif //MIDITRANSPOSER_ARPPANEL_H 31 | -------------------------------------------------------------------------------- /src/gui/panels/IntervalsPanel.cpp: -------------------------------------------------------------------------------- 1 | #include "IntervalsPanel.h" 2 | #include "gui/lookandfeel/BaseLookAndFeel.h" 3 | #include "gui/panels/MainPanel.h" 4 | 5 | namespace Gui 6 | { 7 | 8 | IntervalsPanel::IntervalsPanel(const Params::NoteParam& noteParam) 9 | : juce::Component("Intervals Panel " + noteParam.noteName) 10 | { 11 | intervalsChoices.reserve(kNbIntervals); 12 | for (auto i = 0; i < kNbIntervals; i++) { 13 | intervalsChoices.emplace_back(new AttachedComponent( 14 | *noteParam.intervals[static_cast(i)]->interval, *this, 15 | [](Gui::CustomSlider& slider) { 16 | slider.setNormalisableRange({ -12, 12, 1 }); 17 | }, 18 | // Extra constructor params 19 | "Interval transpose " + noteParam.noteName + "_" + std::to_string(i), juce::Slider::SliderStyle::LinearVertical 20 | )); 21 | } 22 | 23 | transpose = std::make_unique >( 24 | *noteParam.transpose, *this, 25 | [](Gui::CustomSlider& slider) { 26 | slider.setNormalisableRange({ -12, 12, 1 }); 27 | slider.setTooltip("Choose the number of semitones you want to transpose the note."); 28 | slider.setCustomPaintLambda([&slider](juce::Graphics& g) { 29 | const auto value = slider.getValue(); 30 | auto text = (value > 0 ? "+" : "") + juce::String(value); 31 | g.setFont(LnF::getDefaultFont(26.f)); 32 | g.drawText(text, slider.getLocalBounds(), juce::Justification::centred); 33 | }); 34 | }, 35 | // Extra constructor parameters 36 | "Note Transpose " + noteParam.noteName, juce::Slider::SliderStyle::RotaryVerticalDrag 37 | ); 38 | 39 | mapChoice = std::make_unique>( 40 | *noteParam.mapNote, *this, 41 | [this](Gui::TextSwitch& b) { 42 | b.onStateChange = [this]() { 43 | resized(); 44 | }; 45 | b.setCustomTooltipLambda([&b]() { 46 | return juce::String(b.getToggleState() ? "Dea" : "A") + "ctivate the transposition of this note."; 47 | }); 48 | }, 49 | "Note Transpose " + noteParam.noteName + " toggle", "ON", "OFF", 14.f 50 | ); 51 | } 52 | 53 | void IntervalsPanel::resized() 54 | { 55 | if (auto* main_panel = findParentComponentOfClass(); main_panel) { 56 | const auto& coordinates = main_panel->getCoordinates(); 57 | 58 | auto& transpose_slider = transpose->getComponent(); 59 | transpose_slider.setBounds(juce::Rectangle(coordinates.mIntervalsX, 0.f, 60 | coordinates.mIntervalKnobW, 61 | coordinates.mIntervalsH) 62 | .reduced(coordinates.mMargin, coordinates.mMargin * 5.f) 63 | .toNearestInt()); 64 | 65 | auto& map_choice_button = mapChoice->getComponent(); 66 | map_choice_button.setBounds(coordinates.mIntervalsToggle.toNearestInt()); 67 | 68 | juce::FlexBox fb; 69 | fb.flexDirection = juce::FlexBox::Direction::row; 70 | fb.justifyContent = juce::FlexBox::JustifyContent::spaceBetween; 71 | 72 | for (auto& interval: intervalsChoices) { 73 | fb.items.add(juce::FlexItem(interval->getComponent()).withFlex(1)); 74 | } 75 | fb.performLayout(juce::Rectangle(coordinates.mIntervalsX + coordinates.mIntervalKnobW, 0.f, 76 | coordinates.mIntervalsSlidersW, 77 | static_cast(getHeight())) 78 | .reduced(coordinates.mMargin)); 79 | } 80 | } 81 | 82 | } -------------------------------------------------------------------------------- /src/gui/panels/IntervalsPanel.h: -------------------------------------------------------------------------------- 1 | #ifndef MIDITRANSPOSER_INTERVALSPANEL_H 2 | #define MIDITRANSPOSER_INTERVALSPANEL_H 3 | 4 | #include "gui/widgets/Helpers.h" 5 | #include "gui/widgets/CustomSlider.h" 6 | #include "gui/widgets/TextSwitch.h" 7 | 8 | namespace Gui 9 | { 10 | 11 | /** 12 | * @brief This panel holds the 12 interval buttons for a note + a semitone transposition slider. 13 | */ 14 | class IntervalsPanel : public juce::Component 15 | { 16 | public: 17 | IntervalsPanel() = delete; 18 | explicit IntervalsPanel(const Params::NoteParam& noteParam); 19 | 20 | void resized() override; 21 | 22 | private: 23 | /** 24 | * @brief Global transpose knob of the note 25 | */ 26 | std::unique_ptr> transpose; 27 | /** 28 | * @brief List of all the possible intervals 29 | */ 30 | std::vector>> intervalsChoices; 31 | /** 32 | * @brief Button to toggle the mapping 33 | */ 34 | std::unique_ptr> mapChoice; 35 | 36 | static constexpr int kNbIntervals { 12 }; 37 | 38 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (IntervalsPanel) 39 | }; 40 | 41 | } 42 | 43 | #endif //MIDITRANSPOSER_INTERVALSPANEL_H 44 | -------------------------------------------------------------------------------- /src/gui/panels/KeysPanel.cpp: -------------------------------------------------------------------------------- 1 | #include "KeysPanel.h" 2 | #include "gui/panels/MainPanel.h" 3 | 4 | namespace Gui 5 | { 6 | 7 | KeysPanel::KeysPanel() : juce::Component("Keys Panel") 8 | { 9 | noteKeys.reserve(kNbNotes); 10 | 11 | for (size_t i = 0; i < kNbNotes; i++) { 12 | noteKeys.emplace_back(new Gui::NoteKey(static_cast(i))); 13 | addAndMakeVisible(noteKeys.back().get()); 14 | } 15 | } 16 | 17 | void KeysPanel::setNoteKeyEdited(const int index) 18 | { 19 | for (auto& noteKey: noteKeys) { 20 | noteKey->setEdited(index); 21 | } 22 | } 23 | 24 | void KeysPanel::setNotePlayed(const int index) 25 | { 26 | for (auto& noteKey: noteKeys) { 27 | noteKey->setPlayed(index); 28 | } 29 | } 30 | 31 | void KeysPanel::resized() 32 | { 33 | if (auto* main_panel = findParentComponentOfClass(); main_panel) { 34 | const auto& coordinates = main_panel->getCoordinates(); 35 | // Keys are drawn on 2 lines, white keys at the bottom and black keys at the top. 36 | auto line_height = static_cast(getLocalBounds().reduced(static_cast(coordinates.mMargin)).getHeight()) / 2.f; 37 | // Keys are square and have a small margin 38 | auto keys_side = line_height * coordinates.mKeyRatio; 39 | auto keys_margin = line_height * 0.1f; 40 | auto white_keys_side_margin = 41 | (static_cast(getLocalBounds().getWidth()) - (7.f * keys_side + 6.f * keys_margin)) / 2.f; 42 | auto black_keys_side_margin = white_keys_side_margin + keys_side / 2.f; 43 | auto white_key = 0, black_key = 0; 44 | for (size_t i = 0; i < noteKeys.size(); i++) { 45 | float x, y; 46 | if (Notes::whiteNotes[i]) { 47 | x = white_keys_side_margin + static_cast(white_key) * (keys_side + keys_margin); 48 | y = line_height + coordinates.mMargin; 49 | white_key++; 50 | } else { 51 | // Leave an extra space between E and F. 52 | if (black_key == 2) { 53 | black_key++; 54 | } 55 | x = black_keys_side_margin + static_cast(black_key) * (keys_side + keys_margin); 56 | y = coordinates.mMargin; 57 | black_key++; 58 | } 59 | noteKeys[i]->setBounds(static_cast(x), static_cast(y), 60 | static_cast(keys_side), static_cast(line_height)); 61 | noteKeys[i]->resized(); 62 | } 63 | } 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /src/gui/panels/KeysPanel.h: -------------------------------------------------------------------------------- 1 | #ifndef MIDITRANSPOSER_KEYSPANEL_H 2 | #define MIDITRANSPOSER_KEYSPANEL_H 3 | 4 | #include "params/Params.h" 5 | #include "gui/widgets/NoteKey.h" 6 | 7 | namespace Gui 8 | { 9 | /** 10 | * @brief The header contains all the note names + their transposition sliders. 11 | */ 12 | class KeysPanel : public juce::Component 13 | { 14 | public: 15 | KeysPanel(); 16 | 17 | void resized() override; 18 | 19 | std::vector>& getNoteKeys() { return noteKeys; } 20 | void setNoteKeyEdited(int index); 21 | void setNotePlayed(int index); 22 | private: 23 | std::vector> noteKeys; 24 | 25 | static constexpr auto kNbNotes = 12; 26 | 27 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (KeysPanel) 28 | }; 29 | } 30 | 31 | #endif //MIDITRANSPOSER_KEYSPANEL_H 32 | -------------------------------------------------------------------------------- /src/gui/panels/MainPanel.cpp: -------------------------------------------------------------------------------- 1 | #include "MainPanel.h" 2 | 3 | #include "gui/lookandfeel/BaseLookAndFeel.h" 4 | 5 | namespace Gui 6 | { 7 | 8 | MainPanel::MainPanel(MidiTransposerAudioProcessor& p) 9 | : juce::Component("Main Panel"), 10 | mConfiguration(CONFIG_FOLDER, this), 11 | presetsPanel(p.getPresetManager()), 12 | midiPanel(p), 13 | arpPanel(p) 14 | { 15 | mConfiguration.addListener(this); 16 | 17 | auto& uiSettings = p.getUISettings(); 18 | auto& noteParams = p.getMidiProcessor().getNoteParams(); 19 | const auto index = uiSettings.lastNoteIndex; 20 | 21 | updateNoteEdited(index); 22 | if (index > -1) { 23 | auto noteParam = noteParams.notes[static_cast(index)].get(); 24 | initIntervalsPanel(*noteParam); 25 | } 26 | 27 | for (auto& noteKey: keysPanel.getNoteKeys()) { 28 | noteKey->setChangeNoteCallback([this, &uiSettings, ¬eParams](int idx) { 29 | if (uiSettings.lastNoteIndex == idx) { 30 | intervalsPanel.reset(nullptr); 31 | dummyPanel.setVisible(true); 32 | resized(); 33 | idx = -1; 34 | } else { 35 | auto noteParam = noteParams.notes[static_cast(idx)].get(); 36 | initIntervalsPanel(*noteParam); 37 | } 38 | 39 | updateNoteEdited(idx); 40 | uiSettings.lastNoteIndex = idx; 41 | }); 42 | } 43 | 44 | // Get the last note played from the audio processor. 45 | getNotePlayed = [&p]() { 46 | return p.getMidiProcessor().getLastNoteOn(); 47 | }; 48 | 49 | // TODO: custom dummy panel that displays all the transpositions 50 | // Invert label color so it's black on white. 51 | dummyPanel.setColour(juce::Label::ColourIds::textColourId, juce::Colours::black); 52 | dummyPanel.setColour(juce::Label::ColourIds::backgroundColourId, juce::Colours::whitesmoke); 53 | dummyPanel.setFont(LnF::getDefaultFont(28.f)); 54 | dummyPanel.setJustificationType(juce::Justification::centred); 55 | 56 | addAndMakeVisible(presetsPanel); 57 | addAndMakeVisible(midiPanel); 58 | addAndMakeVisible(arpPanel); 59 | addAndMakeVisible(dummyPanel); 60 | addAndMakeVisible(keysPanel); 61 | addAndMakeVisible(tooltipPanel); 62 | 63 | startTimerHz(20); 64 | } 65 | 66 | MainPanel::~MainPanel() 67 | { 68 | mConfiguration.removeListener(this); 69 | } 70 | 71 | void MainPanel::onConfigChanged(const CompCoordinates&) 72 | { 73 | resized(); 74 | } 75 | 76 | void MainPanel::timerCallback() 77 | { 78 | if (getNotePlayed) { 79 | const auto note = getNotePlayed(); 80 | keysPanel.setNotePlayed(note); 81 | } 82 | } 83 | 84 | void MainPanel::paint(juce::Graphics& g) 85 | { 86 | // Draw all the frames for the different panels. 87 | const auto& coordinates = getCoordinates(); 88 | 89 | // Midi frame 90 | g.setColour(findColour(juce::Label::ColourIds::backgroundColourId)); 91 | g.drawRoundedRectangle(coordinates.mMidiPanel.reduced(coordinates.mMargin), 92 | coordinates.mFrameCorner, 1.f); 93 | const auto midi_header_coordinates = juce::Rectangle(0.f, 0.f, 94 | coordinates.mMidiPanel.getWidth(), 95 | coordinates.mHeaderHeight).reduced(coordinates.mMargin * 2.f); 96 | g.fillRoundedRectangle(midi_header_coordinates, coordinates.mFrameCorner / 2.f); 97 | g.drawRoundedRectangle(coordinates.mMidiLabels, coordinates.mKeyCorner, 1.f); 98 | g.drawRoundedRectangle(coordinates.mMidiOct, coordinates.mKeyCorner, 1.f); 99 | 100 | // Arp frame 101 | g.drawRoundedRectangle(coordinates.mArpPanel.reduced(coordinates.mMargin), 102 | coordinates.mFrameCorner, 1.f); 103 | const auto arp_header_x = coordinates.mArpPanel.getX(); 104 | const auto arp_header_coordinates = juce::Rectangle(arp_header_x, 0.f, 105 | coordinates.mArpPanel.getWidth(), 106 | coordinates.mHeaderHeight).reduced(coordinates.mMargin * 2.f); 107 | g.fillRoundedRectangle(arp_header_coordinates, coordinates.mFrameCorner / 2.f); 108 | 109 | // Presets frame 110 | const auto presets_panel_x = coordinates.mPresetsPanel.getX(); 111 | const auto presets_panel_width = static_cast(coordinates.mPresetsPanel.getWidth()); 112 | const auto presets_panel_height = static_cast(coordinates.mPresetsPanel.getHeight()); 113 | g.drawRoundedRectangle(juce::Rectangle(presets_panel_x, 0.f, 114 | presets_panel_width, presets_panel_height).reduced(coordinates.mMargin), 115 | coordinates.mFrameCorner, 1.f); 116 | const auto preset_header_coordinates = juce::Rectangle(presets_panel_x, 0.f, 117 | presets_panel_width, 118 | coordinates.mHeaderHeight).reduced(coordinates.mMargin * 2.f); 119 | g.fillRoundedRectangle(preset_header_coordinates, coordinates.mFrameCorner / 2.f); 120 | 121 | // Tooltips frame 122 | g.drawRoundedRectangle(coordinates.mTooltipsPanel.reduced(coordinates.mMargin), 123 | coordinates.mFrameCorner, 1.f); 124 | 125 | // Intervals frame 126 | g.drawRoundedRectangle(coordinates.mIntervalsPanel.reduced(coordinates.mMargin), 127 | coordinates.mFrameCorner, 1.f); 128 | g.drawRoundedRectangle(coordinates.mIntervalsTransposeLabel, coordinates.mIntervalsLabelCorner, 1.f); 129 | g.drawRoundedRectangle(coordinates.mIntervalsSlidersLabel, coordinates.mIntervalsLabelCorner, 1.f); 130 | 131 | // Header texts 132 | g.setColour(findColour(juce::Label::ColourIds::textColourId)); 133 | g.setFont(LnF::getDefaultFont(coordinates.mHeaderFontSize)); 134 | g.drawText("MIDI", midi_header_coordinates, juce::Justification::centred); 135 | g.drawText("ARP", arp_header_coordinates, juce::Justification::centred); 136 | g.drawText("PRESETS", preset_header_coordinates, juce::Justification::centred); 137 | 138 | // Label texts 139 | g.setFont(coordinates.mLabelFontSize); 140 | g.setColour(findColour(juce::Label::ColourIds::backgroundColourId)); 141 | g.drawText("IN", coordinates.mMidiLabels.withX(coordinates.mMidiInX), juce::Justification::left); 142 | g.drawText("OUT", coordinates.mMidiLabels.withX(coordinates.mMidiOutX), juce::Justification::left); 143 | g.drawText(juce::CharPointer_UTF8("OCT±"), coordinates.mMidiOct.withX(coordinates.mMidiOctX), juce::Justification::left); 144 | 145 | g.setFont(coordinates.mIntervalsLabelFontSize); 146 | g.drawText("Transpose", coordinates.mIntervalsTransposeLabel, juce::Justification::centred); 147 | } 148 | 149 | void MainPanel::resized() 150 | { 151 | const auto& coordinates = getCoordinates(); 152 | 153 | // Define if there's an interval panel to display or not. 154 | juce::Component* middleItem = nullptr; 155 | if (intervalsPanel != nullptr) { 156 | middleItem = intervalsPanel.get(); 157 | middleItem->setBounds(coordinates.mIntervalsPanel.toNearestInt()); 158 | } else { 159 | middleItem = &dummyPanel; 160 | middleItem->setBounds(coordinates.mIntervalsPanel.toNearestInt().reduced(static_cast(coordinates.mMargin) * 2)); 161 | } 162 | 163 | midiPanel.setBounds(coordinates.mMidiPanel.toNearestInt()); 164 | arpPanel.setBounds(coordinates.mArpPanel.toNearestInt()); 165 | presetsPanel.setBounds(coordinates.mPresetsPanel.toNearestInt()); 166 | keysPanel.setBounds(coordinates.mKeysPanel.toNearestInt()); 167 | tooltipPanel.setBounds(coordinates.mTooltipsPanel.toNearestInt().reduced(static_cast(coordinates.mMargin))); 168 | 169 | // Force resized on children because it's not triggered when using a transform to scale components. 170 | for (auto* child: getChildren()) { 171 | child->resized(); 172 | } 173 | } 174 | 175 | void MainPanel::initIntervalsPanel(Params::NoteParam& noteParam) 176 | { 177 | intervalsPanel = std::make_unique(noteParam); 178 | dummyPanel.setVisible(false); 179 | addAndMakeVisible(intervalsPanel.get()); 180 | resized(); 181 | } 182 | 183 | void MainPanel::updateNoteEdited(const int index) 184 | { 185 | keysPanel.setNoteKeyEdited(index); 186 | repaint(); 187 | } 188 | 189 | } -------------------------------------------------------------------------------- /src/gui/panels/MainPanel.h: -------------------------------------------------------------------------------- 1 | #ifndef MIDITRANSPOSER_MAINPANEL_H 2 | #define MIDITRANSPOSER_MAINPANEL_H 3 | 4 | #include "processor/PluginProcessor.h" 5 | #include "gui/panels/KeysPanel.h" 6 | #include "gui/panels/IntervalsPanel.h" 7 | #include "gui/panels/PresetsPanel.h" 8 | #include "gui/panels/MidiPanel.h" 9 | #include "gui/panels/ArpPanel.h" 10 | #include "gui/Configuration.hpp" 11 | #include "gui/CompCoordinates.h" 12 | 13 | namespace Gui 14 | { 15 | 16 | class MainPanel : public juce::Component, 17 | public Configuration::Listener, 18 | public juce::Timer 19 | { 20 | public: 21 | MainPanel() = delete; 22 | explicit MainPanel(MidiTransposerAudioProcessor& p); 23 | ~MainPanel() override; 24 | 25 | void resized() override; 26 | void paint(juce::Graphics& g) override; 27 | 28 | juce::Component* getTooltipPanel() { return &tooltipPanel; } 29 | 30 | [[nodiscard]] const CompCoordinates& getCoordinates() const { return mConfiguration.getData(); } 31 | 32 | void timerCallback() override; 33 | private: 34 | Configuration mConfiguration; 35 | 36 | std::function getNotePlayed = nullptr; 37 | 38 | void onConfigChanged(const CompCoordinates& positions) override; 39 | 40 | /** 41 | * @brief Resets the intervals panel with the currently edited note. 42 | * @param noteParam the note that is edited. 43 | */ 44 | void initIntervalsPanel(Params::NoteParam& noteParam); 45 | 46 | /** 47 | * @brief Notifies all the note keys to tell which one is edited and update its color. 48 | * @param index the index of the edited note. -1 means there's no note currently edited. 49 | */ 50 | void updateNoteEdited(int index); 51 | 52 | /** 53 | * @brief Contains the name of the current preset + buttons to load/save/reset presets 54 | */ 55 | PresetsPanel presetsPanel; 56 | 57 | /** 58 | * @brief Contains the midi settings. 59 | */ 60 | MidiPanel midiPanel; 61 | 62 | /** 63 | * @brief Contains the arpeggiator settings. 64 | */ 65 | ArpPanel arpPanel; 66 | 67 | /** 68 | * @brief Fills the empty space when not editing a key. Contains some instruction. 69 | */ 70 | juce::Label dummyPanel { "Dummy Panel", "Click on a key to configure the associated chord" }; 71 | 72 | /** 73 | * @brief Contains the twelve keys that trigger the display of the intervals panel. 74 | */ 75 | KeysPanel keysPanel; 76 | 77 | /** 78 | * @brief Contains toggle buttons to activate the intervals for a certain key. 79 | */ 80 | std::unique_ptr intervalsPanel = nullptr; 81 | 82 | /** 83 | * @brief Container that will be put at the bottom to display all the tooltips at the same place. 84 | */ 85 | juce::Component tooltipPanel { "tooltipPanel" }; 86 | 87 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainPanel) 88 | }; 89 | 90 | } 91 | 92 | #endif //MIDITRANSPOSER_MAINPANEL_H 93 | -------------------------------------------------------------------------------- /src/gui/panels/MidiPanel.cpp: -------------------------------------------------------------------------------- 1 | #include "MidiPanel.h" 2 | #include "gui/lookandfeel/BaseLookAndFeel.h" 3 | #include "gui/panels/MainPanel.h" 4 | 5 | namespace Gui 6 | { 7 | 8 | MidiPanel::MidiPanel(MidiTransposerAudioProcessor& p) 9 | : juce::Component("Midi Panel") 10 | { 11 | auto& midiParams = p.getMidiProcessor().getMidiParams(); 12 | 13 | inputChannel = std::make_unique< AttachedComponent >( 14 | *midiParams.inputChannel, *this, 15 | [](Gui::CustomSlider& slider) { 16 | slider.setNormalisableRange({0, 16, 1}); 17 | slider.setTooltip("Only the events coming from this channel will be transposed. The rest will pass through."); 18 | slider.setCustomPaintLambda([&slider](juce::Graphics& g) { 19 | auto text = juce::String(slider.getValue()); 20 | g.setFont(LnF::getDefaultFont(26.f)); 21 | g.drawText(text, slider.getLocalBounds(), juce::Justification::centred); 22 | }); 23 | }, 24 | "Midi Input Slider", juce::Slider::SliderStyle::RotaryVerticalDrag 25 | ); 26 | 27 | outputChannel = std::make_unique< AttachedComponent >( 28 | *midiParams.outputChannel, *this, 29 | [](Gui::CustomSlider& slider) { 30 | slider.setNormalisableRange({0, 16, 1}); 31 | slider.setTooltip("The transposed events will be routed to this channel."); 32 | slider.setCustomPaintLambda([&slider](juce::Graphics& g) { 33 | auto text = juce::String(slider.getValue()); 34 | g.setFont(LnF::getDefaultFont(26.f)); 35 | g.drawText(text, slider.getLocalBounds(), juce::Justification::centred); 36 | }); 37 | }, 38 | "Midi Output Slider", juce::Slider::SliderStyle::RotaryVerticalDrag 39 | ); 40 | 41 | octaveTranspose = std::make_unique< AttachedComponent >( 42 | *midiParams.octaveTranspose, *this, 43 | [](Gui::CustomSlider& slider) { 44 | slider.setNormalisableRange({-1, 4, 1}); 45 | slider.setTooltip("This will play the root note at its original position and transpose the chord."); 46 | slider.setCustomPaintLambda([&slider](juce::Graphics& g) { 47 | const auto value = slider.getValue(); 48 | auto text = (value > 0 ? "+" : "") + juce::String(value); 49 | g.setFont(LnF::getDefaultFont(26.f)); 50 | g.drawText(text, slider.getLocalBounds(), juce::Justification::centred); 51 | }); 52 | }, 53 | "Octave Slider", juce::Slider::SliderStyle::RotaryVerticalDrag 54 | ); 55 | } 56 | 57 | void MidiPanel::resized() 58 | { 59 | using juce::operator""_px; 60 | using juce::operator""_fr; 61 | 62 | juce::Grid grid; 63 | using Track = juce::Grid::TrackInfo; 64 | 65 | grid.templateRows = { Track (1_fr) }; 66 | grid.templateColumns = { Track (1_fr), Track (1_fr), Track (1_fr) }; 67 | 68 | grid.alignContent = juce::Grid::AlignContent::center; 69 | grid.justifyContent = juce::Grid::JustifyContent::center; 70 | grid.alignItems = juce::Grid::AlignItems::center; 71 | grid.justifyItems = juce::Grid::JustifyItems::center; 72 | 73 | grid.columnGap = 2_px; 74 | grid.rowGap = 2_px; 75 | 76 | grid.items = { 77 | juce::GridItem(inputChannel->getComponent()), 78 | juce::GridItem(outputChannel->getComponent()), 79 | juce::GridItem(octaveTranspose->getComponent()) 80 | }; 81 | 82 | if (auto* main_panel = findParentComponentOfClass(); main_panel) { 83 | const auto& coordinates = main_panel->getCoordinates(); 84 | grid.performLayout(juce::Rectangle(0, static_cast(coordinates.mHeaderHeight), 85 | getWidth(), static_cast(coordinates.mKnobHeight)) 86 | .reduced(static_cast(coordinates.mMargin))); 87 | } 88 | } 89 | 90 | } -------------------------------------------------------------------------------- /src/gui/panels/MidiPanel.h: -------------------------------------------------------------------------------- 1 | #ifndef MIDITRANSPOSER_MIDIPANEL_H 2 | #define MIDITRANSPOSER_MIDIPANEL_H 3 | 4 | #include "processor/PluginProcessor.h" 5 | #include "gui/widgets/Helpers.h" 6 | #include "gui/widgets/CustomSlider.h" 7 | 8 | namespace Gui 9 | { 10 | 11 | /** 12 | * @brief The header with MIDI params and Arpeggiator params + preset manager 13 | */ 14 | class MidiPanel : public juce::Component 15 | { 16 | public: 17 | MidiPanel() = delete; 18 | explicit MidiPanel(MidiTransposerAudioProcessor& p); 19 | 20 | void resized() override; 21 | 22 | private: 23 | std::unique_ptr< AttachedComponent > inputChannel; 24 | std::unique_ptr< AttachedComponent > outputChannel; 25 | std::unique_ptr< AttachedComponent > octaveTranspose; 26 | 27 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiPanel) 28 | }; 29 | 30 | } 31 | #endif //MIDITRANSPOSER_MIDIPANEL_H 32 | -------------------------------------------------------------------------------- /src/gui/panels/PresetsPanel.cpp: -------------------------------------------------------------------------------- 1 | #include "PresetsPanel.h" 2 | #include "BinaryData.h" 3 | #include "gui/panels/MainPanel.h" 4 | 5 | namespace Gui 6 | { 7 | 8 | /** 9 | * @brief A callback struct used by the AlertWindow to validate the user input. 10 | */ 11 | struct PresetNameDialogChosen 12 | { 13 | void operator()(const int result) const noexcept 14 | { 15 | panel.validatePresetSave(result); 16 | } 17 | 18 | PresetsPanel& panel; 19 | }; 20 | 21 | PresetsPanel::PresetsPanel(PresetBrowser::PresetManager& pm) 22 | : juce::Component("Presets Panel"), 23 | presetManager(pm) 24 | { 25 | addAndMakeVisible(presetListComboBox); 26 | presetListComboBox.setTextWhenNothingSelected(PresetBrowser::PresetManager::kInitPreset); 27 | presetListComboBox.setMouseCursor(juce::MouseCursor::PointingHandCursor); 28 | presetListComboBox.addListener(this); 29 | updatePresetsList(); 30 | 31 | const auto new_icon = juce::Drawable::createFromImageData(BinaryData::new_svg, BinaryData::new_svgSize); 32 | const auto save_icon = juce::Drawable::createFromImageData(BinaryData::save_svg, BinaryData::save_svgSize); 33 | const auto copy_icon = juce::Drawable::createFromImageData(BinaryData::copy_svg, BinaryData::copy_svgSize); 34 | const auto delete_icon = juce::Drawable::createFromImageData(BinaryData::delete_svg, BinaryData::delete_svgSize); 35 | 36 | const auto previous_icon = juce::Drawable::createFromImageData(BinaryData::previous_svg, BinaryData::previous_svgSize); 37 | const auto next_icon = juce::Drawable::createFromImageData(BinaryData::next_svg, BinaryData::next_svgSize); 38 | 39 | initButton(presetNewButton, &*new_icon, "Create a new preset"); 40 | initButton(presetSaveButton, &*save_icon, "Save the current preset"); 41 | initButton(presetCopyButton, &*copy_icon, "Copy the current preset"); 42 | initButton(presetDeleteButton, &*delete_icon, "Delete the current preset"); 43 | 44 | initButton(presetPreviousButton, &*previous_icon, "Load the previous preset"); 45 | initButton(presetNextButton, &*next_icon, "Load the next preset"); 46 | } 47 | 48 | PresetsPanel::~PresetsPanel() 49 | { 50 | presetSaveButton.removeListener(this); 51 | presetNewButton.removeListener(this); 52 | presetDeleteButton.removeListener(this); 53 | presetListComboBox.removeListener(this); 54 | } 55 | 56 | void PresetsPanel::initButton(juce::DrawableButton& ioButton, const juce::Drawable* inDrawable, const juce::String& inTooltip) 57 | { 58 | addAndMakeVisible(ioButton); 59 | ioButton.setImages(inDrawable); 60 | ioButton.setMouseCursor(juce::MouseCursor::PointingHandCursor); 61 | ioButton.setTooltip(inTooltip); 62 | ioButton.addListener(this); 63 | } 64 | 65 | void PresetsPanel::buttonClicked(juce::Button* button) 66 | { 67 | if (button == &presetCopyButton) { 68 | savePreset(); 69 | return; 70 | } 71 | 72 | const auto& current_preset = presetManager.getCurrentPreset(); 73 | if (button == &presetSaveButton) { 74 | if (current_preset == PresetBrowser::PresetManager::kInitPreset) { 75 | savePreset(); 76 | } else { 77 | presetManager.savePreset(current_preset); 78 | updatePresetsList(); 79 | } 80 | return; 81 | } 82 | 83 | if (button == &presetDeleteButton) { 84 | presetManager.deletePreset(current_preset); 85 | updatePresetsList(); 86 | return; 87 | } 88 | 89 | if (button == &presetNewButton) { 90 | presetManager.resetPreset(); 91 | presetListComboBox.setSelectedItemIndex(-1, juce::NotificationType::dontSendNotification); 92 | return; 93 | } 94 | 95 | if (button == &presetPreviousButton) { 96 | loadPreset(-1); 97 | return; 98 | } 99 | 100 | if (button == &presetNextButton) { 101 | loadPreset(1); 102 | return; 103 | } 104 | } 105 | 106 | void PresetsPanel::loadPreset(const int offset) 107 | { 108 | auto new_index = presetListComboBox.getSelectedItemIndex() + offset; 109 | // Make the index loop around. 110 | new_index = new_index < 0 ? presetListComboBox.getNumItems() - 1 : new_index % presetListComboBox.getNumItems(); 111 | presetManager.loadPreset(presetListComboBox.getItemText(new_index)); 112 | presetListComboBox.setSelectedItemIndex(new_index, juce::NotificationType::dontSendNotification); 113 | } 114 | 115 | void PresetsPanel::savePreset() 116 | { 117 | presetNameChooser = std::make_unique("Save this preset", 118 | "Choose the name for your preset.", 119 | juce::MessageBoxIconType::NoIcon); 120 | 121 | presetNameChooser->addTextEditor("preset", presetManager.getCurrentPreset(), "Preset name :"); 122 | presetNameChooser->addButton("OK", 1, juce::KeyPress(juce::KeyPress::returnKey, 0, 0)); 123 | presetNameChooser->addButton("Cancel", 0, juce::KeyPress(juce::KeyPress::escapeKey, 0, 0)); 124 | 125 | presetNameChooser->enterModalState(true, juce::ModalCallbackFunction::create(PresetNameDialogChosen{*this})); 126 | } 127 | 128 | void PresetsPanel::comboBoxChanged(juce::ComboBox* comboBoxThatHasChanged) 129 | { 130 | if (comboBoxThatHasChanged == &presetListComboBox) { 131 | presetManager.loadPreset(presetListComboBox.getItemText(presetListComboBox.getSelectedItemIndex())); 132 | } 133 | } 134 | 135 | void PresetsPanel::updatePresetsList() 136 | { 137 | presetListComboBox.clear(juce::dontSendNotification); 138 | const auto allPresets = presetManager.getAllPresets(); 139 | const auto currentPreset = presetManager.getCurrentPreset(); 140 | presetListComboBox.addItemList(allPresets, 1); 141 | presetListComboBox.setSelectedItemIndex(allPresets.indexOf(currentPreset), juce::dontSendNotification); 142 | } 143 | 144 | void PresetsPanel::validatePresetSave(const int result) 145 | { 146 | presetNameChooser->exitModalState(result); 147 | presetNameChooser->setVisible(false); 148 | 149 | if (result == 0) { 150 | return; 151 | } 152 | 153 | const auto preset_name = presetNameChooser->getTextEditorContents("preset"); 154 | //TODO: Some input validation. 155 | if (presetManager.savePreset(preset_name)) { 156 | updatePresetsList(); 157 | } 158 | } 159 | 160 | void PresetsPanel::resized() 161 | { 162 | juce::FlexBox file_buttons; 163 | file_buttons.flexDirection = juce::FlexBox::Direction::row; 164 | file_buttons.justifyContent = juce::FlexBox::JustifyContent::spaceAround; 165 | 166 | file_buttons.items.add(juce::FlexItem(presetNewButton).withFlex(1).withMargin(juce::FlexItem::Margin(0, 5, 0, 5))); 167 | file_buttons.items.add(juce::FlexItem(presetSaveButton).withFlex(1).withMargin(juce::FlexItem::Margin(0, 5, 0, 5))); 168 | file_buttons.items.add(juce::FlexItem(presetCopyButton).withFlex(1).withMargin(juce::FlexItem::Margin(0, 5, 0, 5))); 169 | file_buttons.items.add(juce::FlexItem(presetDeleteButton).withFlex(1).withMargin(juce::FlexItem::Margin(0, 5, 0, 5))); 170 | 171 | juce::FlexBox preset_buttons; 172 | preset_buttons.flexDirection = juce::FlexBox::Direction::row; 173 | preset_buttons.justifyContent = juce::FlexBox::JustifyContent::spaceAround; 174 | 175 | preset_buttons.items.add(juce::FlexItem(presetPreviousButton).withFlex(1).withMargin(juce::FlexItem::Margin(0, 20, 0, 20))); 176 | preset_buttons.items.add(juce::FlexItem(presetNextButton).withFlex(1).withMargin(juce::FlexItem::Margin(0, 20, 0, 20))); 177 | 178 | if (const auto* main_panel = findParentComponentOfClass(); main_panel) { 179 | const auto& coordinates = main_panel->getCoordinates(); 180 | 181 | // Draw the file buttons icons. 182 | file_buttons.performLayout(juce::Rectangle(0, static_cast(coordinates.mHeaderHeight), 183 | getWidth(), static_cast(coordinates.mButtonHeight)) 184 | .reduced(static_cast(coordinates.mMargin) * 2, 0)); 185 | 186 | // Calculate the combobox coordinates. 187 | const auto combo_bounds = juce::Rectangle(0, static_cast(coordinates.mHeaderHeight) + static_cast(coordinates.mButtonHeight), 188 | getWidth(), static_cast(coordinates.mButtonHeight)) 189 | .reduced(static_cast(coordinates.mMargin) * 2, static_cast(coordinates.mMargin)); 190 | presetListComboBox.setBounds(combo_bounds); 191 | 192 | // Draw the preset navigation buttons just below. 193 | preset_buttons.performLayout(combo_bounds 194 | .translated(0, combo_bounds.getHeight() + static_cast(coordinates.mMargin)) 195 | .withHeight(static_cast(coordinates.mButtonHeight))); 196 | } 197 | } 198 | 199 | } -------------------------------------------------------------------------------- /src/gui/panels/PresetsPanel.h: -------------------------------------------------------------------------------- 1 | #ifndef MIDITRANSPOSER_PRESETSPANEL_H 2 | #define MIDITRANSPOSER_PRESETSPANEL_H 3 | 4 | #include "presets/PresetManager.h" 5 | #include 6 | 7 | namespace Gui 8 | { 9 | /** 10 | * @brief Contains the current preset name + presets browsing/save/load 11 | */ 12 | class PresetsPanel final : public juce::Component, juce::Button::Listener, juce::ComboBox::Listener 13 | { 14 | public: 15 | PresetsPanel() = delete; 16 | explicit PresetsPanel(PresetBrowser::PresetManager& pm); 17 | ~PresetsPanel() override; 18 | 19 | void initButton(juce::DrawableButton& ioButton, const juce::Drawable* inDrawable, const juce::String& inTooltip); 20 | 21 | void resized() override; 22 | 23 | void buttonClicked(juce::Button* button) override; 24 | void comboBoxChanged(juce::ComboBox* comboBoxThatHasChanged) override; 25 | 26 | void validatePresetSave(int result); 27 | private: 28 | void updatePresetsList(); 29 | 30 | /** 31 | * @brief Load a preset from the preset manager 32 | * @param offset Offset from the current preset (-1 for previous, 1 for next) 33 | */ 34 | void loadPreset(int offset); 35 | 36 | void savePreset(); 37 | 38 | PresetBrowser::PresetManager& presetManager; // Only a reference, the audio processor is owning it. 39 | 40 | juce::DrawableButton presetNewButton { "btnNewPreset", juce::DrawableButton::ButtonStyle::ImageFitted }; 41 | juce::DrawableButton presetSaveButton { "btnSavePreset", juce::DrawableButton::ButtonStyle::ImageFitted }; 42 | juce::DrawableButton presetCopyButton { "btnCopyPreset", juce::DrawableButton::ButtonStyle::ImageFitted }; 43 | juce::DrawableButton presetDeleteButton { "btnDeletePreset", juce::DrawableButton::ButtonStyle::ImageFitted }; 44 | 45 | juce::DrawableButton presetPreviousButton { "btnPreviousPreset", juce::DrawableButton::ButtonStyle::ImageFitted }; 46 | juce::DrawableButton presetNextButton { "btnNextPreset", juce::DrawableButton::ButtonStyle::ImageFitted }; 47 | 48 | juce::ComboBox presetListComboBox { "cmbPresetList" }; 49 | 50 | std::unique_ptr presetNameChooser; 51 | 52 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PresetsPanel) 53 | }; 54 | } 55 | 56 | #endif //MIDITRANSPOSER_PRESETSPANEL_H 57 | -------------------------------------------------------------------------------- /src/gui/widgets/CustomSlider.cpp: -------------------------------------------------------------------------------- 1 | #include "CustomSlider.h" 2 | 3 | namespace Gui 4 | { 5 | CustomSlider::CustomSlider(const juce::String& name, 6 | const SliderStyle inStyle, 7 | const TextEntryBoxPosition inPosition) 8 | : juce::Slider(name) 9 | { 10 | // Init default slider form. 11 | setSliderStyle(inStyle); 12 | setTextBoxStyle(inPosition, true, getWidth(), getWidth()); 13 | } 14 | 15 | void CustomSlider::paint(juce::Graphics& g) 16 | { 17 | // Draw the regular slider in every case. 18 | juce::Slider::paint(g); 19 | // Potentially paint something over it. 20 | if (mCustomPaintLambda != nullptr) { 21 | mCustomPaintLambda(g); 22 | } 23 | } 24 | 25 | juce::String CustomSlider::getTextFromValue(double value) 26 | { 27 | if (mCustomTextLambda != nullptr) { 28 | return mCustomTextLambda(value); 29 | } 30 | return juce::Slider::getTextFromValue(value); 31 | } 32 | 33 | void CustomSlider::setCustomTextLambda(std::function inLambda) 34 | { 35 | mCustomTextLambda = std::move(inLambda); 36 | } 37 | 38 | void CustomSlider::setCustomPaintLambda(std::function inLambda) 39 | { 40 | mCustomPaintLambda = std::move(inLambda); 41 | } 42 | } // Gui -------------------------------------------------------------------------------- /src/gui/widgets/CustomSlider.h: -------------------------------------------------------------------------------- 1 | #ifndef MIDITRANSPOSER_CUSTOMSLIDER_H 2 | #define MIDITRANSPOSER_CUSTOMSLIDER_H 3 | 4 | #include "params/Params.h" 5 | 6 | namespace Gui 7 | { 8 | 9 | class CustomSlider final : public juce::Slider 10 | { 11 | public: 12 | explicit CustomSlider(const juce::String& name, 13 | SliderStyle inStyle = Rotary, 14 | TextEntryBoxPosition inPosition = NoTextBox); 15 | 16 | void setCustomTextLambda(std::function inLambda); 17 | 18 | void setCustomPaintLambda(std::function inLambda); 19 | 20 | void paint(juce::Graphics& g) override; 21 | 22 | juce::String getTextFromValue(double value) override; 23 | 24 | private: 25 | std::function mCustomTextLambda = nullptr; 26 | 27 | std::function mCustomPaintLambda = nullptr; 28 | }; 29 | 30 | } // Gui 31 | 32 | #endif //MIDITRANSPOSER_CUSTOMSLIDER_H 33 | -------------------------------------------------------------------------------- /src/gui/widgets/CustomToggleButton.cpp: -------------------------------------------------------------------------------- 1 | #include "CustomToggleButton.h" 2 | 3 | namespace Gui 4 | { 5 | CustomToggleButton::CustomToggleButton(const juce::String& name) 6 | : juce::ToggleButton(name) 7 | { } 8 | 9 | juce::String CustomToggleButton::getTooltip() 10 | { 11 | if (mCustomTooltipLambda != nullptr) { 12 | return mCustomTooltipLambda(); 13 | } 14 | return juce::ToggleButton::getTooltip(); 15 | } 16 | 17 | void CustomToggleButton::mouseEnter(const juce::MouseEvent&) 18 | { 19 | setMouseCursor(juce::MouseCursor::PointingHandCursor); 20 | } 21 | 22 | void CustomToggleButton::mouseExit(const juce::MouseEvent&) 23 | { 24 | setMouseCursor(juce::MouseCursor::NormalCursor); 25 | } 26 | 27 | void CustomToggleButton::setCustomTooltipLambda(std::function inLambda) 28 | { 29 | mCustomTooltipLambda = std::move(inLambda); 30 | } 31 | } -------------------------------------------------------------------------------- /src/gui/widgets/CustomToggleButton.h: -------------------------------------------------------------------------------- 1 | #ifndef MIDITRANSPOSER_CUSTOMTOGGLEBUTTON_H 2 | #define MIDITRANSPOSER_CUSTOMTOGGLEBUTTON_H 3 | 4 | #include 5 | #include 6 | 7 | namespace Gui 8 | { 9 | 10 | class CustomToggleButton : public juce::ToggleButton 11 | { 12 | public: 13 | explicit CustomToggleButton(const juce::String& name); 14 | 15 | juce::String getTooltip() override; 16 | 17 | void mouseEnter(const juce::MouseEvent& event) override; 18 | void mouseExit(const juce::MouseEvent& event) override; 19 | 20 | void setCustomTooltipLambda(std::function inLambda); 21 | 22 | private: 23 | std::function mCustomTooltipLambda = nullptr; 24 | }; 25 | 26 | } // Gui 27 | 28 | #endif //MIDITRANSPOSER_CUSTOMTOGGLEBUTTON_H -------------------------------------------------------------------------------- /src/gui/widgets/Helpers.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "juce_audio_processors/juce_audio_processors.h" 4 | #include "juce_gui_basics/juce_gui_basics.h" 5 | 6 | namespace Gui 7 | { 8 | 9 | /** 10 | * @brief Each gui component linked to a parameter is created as an AttachedComponent to manage the attachment in the same object. 11 | * It's automatically made visible when it's initialised. The init parameter function can be used to define the component properties. 12 | * Some extra args can be passed to construct the component 13 | * @tparam CompType The type of component to display (slider, button, etc.) 14 | * @tparam CompAttachment The ParameterAttachment linked to the component. 15 | */ 16 | template 17 | class AttachedComponent 18 | { 19 | public: 20 | template 21 | AttachedComponent(juce::RangedAudioParameter& param, juce::Component& parent, 22 | std::function init = nullptr, Args&&... args) 23 | : component(std::forward(args)...), 24 | attachment(param, component) 25 | { 26 | parent.addAndMakeVisible(component); 27 | if (init != nullptr) { init(component); } 28 | attachment.sendInitialUpdate(); 29 | } 30 | 31 | CompType& getComponent() { return component; } 32 | private: 33 | CompType component; 34 | CompAttachment attachment; 35 | 36 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AttachedComponent) 37 | }; 38 | 39 | } -------------------------------------------------------------------------------- /src/gui/widgets/NoteKey.cpp: -------------------------------------------------------------------------------- 1 | #include "NoteKey.h" 2 | #include "params/Params.h" 3 | #include "gui/lookandfeel/BaseLookAndFeel.h" 4 | #include "gui/panels/MainPanel.h" 5 | 6 | namespace Gui 7 | { 8 | 9 | NoteKey::NoteKey(const int index) 10 | : TextButton("Note Key " + juce::String(index)), noteIndex(index) 11 | { 12 | Button::setTooltip("Click to change the intervals for this note."); 13 | } 14 | 15 | void NoteKey::setPlayed(const int index) 16 | { 17 | const auto oldPlayed = isPlayed; 18 | isPlayed = (noteIndex == index % 12); 19 | if (oldPlayed != isPlayed) { 20 | repaint(); 21 | } 22 | } 23 | 24 | void NoteKey::paint(juce::Graphics& g) 25 | { 26 | setMouseCursor(juce::MouseCursor::PointingHandCursor); 27 | if (const auto* main_panel = findParentComponentOfClass(); main_panel) { 28 | const auto& coordinates = main_panel->getCoordinates(); 29 | const auto key_bounds = juce::Rectangle(0.f, 0.f, 30 | static_cast(getWidth()), 31 | static_cast(getHeight())).reduced(3.f); 32 | auto draw_key = [&](int background_color, int text_color) { 33 | if (isPlayed) { 34 | background_color = juce::TextButton::ColourIds::buttonColourId; 35 | text_color = juce::TextButton::ColourIds::textColourOnId; 36 | } else if (isEdited) { 37 | background_color = juce::Slider::backgroundColourId; 38 | text_color = juce::Slider::ColourIds::textBoxTextColourId; 39 | } 40 | 41 | if (isOver) { 42 | g.addTransform(juce::AffineTransform::scale(coordinates.mKeyOver)); 43 | } 44 | 45 | g.setColour(findColour(background_color)); 46 | g.fillRoundedRectangle(key_bounds, coordinates.mKeyCorner); 47 | g.setColour(findColour(text_color)); 48 | g.drawRoundedRectangle(key_bounds, coordinates.mKeyCorner, 1.f); 49 | g.setFont(LnF::getDefaultFont(coordinates.mKeyFontSize)); 50 | g.drawText(std::string(Notes::labels[static_cast(noteIndex)]), key_bounds, juce::Justification::centred); 51 | }; 52 | if (Notes::whiteNotes[static_cast(noteIndex)]) { 53 | draw_key(juce::Label::ColourIds::textColourId, juce::Label::ColourIds::backgroundColourId); 54 | } else { 55 | draw_key(juce::Label::ColourIds::backgroundColourId, juce::Label::ColourIds::textColourId); 56 | } 57 | } 58 | } 59 | 60 | void NoteKey::mouseDown(const juce::MouseEvent&) 61 | { 62 | if (changeNote != nullptr) { 63 | changeNote(noteIndex); 64 | } 65 | } 66 | 67 | void NoteKey::mouseEnter(const juce::MouseEvent&) 68 | { 69 | isOver = true; 70 | repaint(); 71 | } 72 | 73 | void NoteKey::mouseExit(const juce::MouseEvent&) 74 | { 75 | isOver = false; 76 | repaint(); 77 | } 78 | 79 | } -------------------------------------------------------------------------------- /src/gui/widgets/NoteKey.h: -------------------------------------------------------------------------------- 1 | #ifndef MIDITRANSPOSER_NOTEKEY_H 2 | #define MIDITRANSPOSER_NOTEKEY_H 3 | 4 | #include 5 | 6 | namespace Gui 7 | { 8 | 9 | /** 10 | * @brief The drawing of a key with an event to display its intervals. 11 | */ 12 | class NoteKey final : public juce::TextButton 13 | { 14 | public: 15 | NoteKey() = delete; 16 | explicit NoteKey(int index); 17 | 18 | void paint(juce::Graphics& g) override; 19 | void mouseDown(const juce::MouseEvent&) override; 20 | void mouseEnter(const juce::MouseEvent&) override; 21 | void mouseExit(const juce::MouseEvent&) override; 22 | 23 | void setEdited(const int index) { isEdited = (noteIndex == index); } 24 | void setPlayed(int index); 25 | void setChangeNoteCallback(std::function inCallback) { changeNote = std::move(inCallback); } 26 | 27 | private: 28 | int noteIndex; 29 | bool isEdited = false; 30 | bool isOver = false; 31 | bool isPlayed = false; 32 | std::function changeNote = nullptr; 33 | 34 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NoteKey) 35 | }; 36 | 37 | } 38 | 39 | #endif //MIDITRANSPOSER_NOTEKEY_H 40 | -------------------------------------------------------------------------------- /src/gui/widgets/TextSwitch.cpp: -------------------------------------------------------------------------------- 1 | #include "TextSwitch.h" 2 | #include "gui/lookandfeel/BaseLookAndFeel.h" 3 | #include "gui/panels/MainPanel.h" 4 | 5 | namespace Gui 6 | { 7 | TextSwitch::TextSwitch(const juce::String& name, juce::String inOnText, juce::String inOffText, float inFontSize) 8 | : Gui::CustomToggleButton(name), onText(std::move(inOnText)), offText(std::move(inOffText)), fontSize(inFontSize) 9 | { } 10 | 11 | void TextSwitch::paint(juce::Graphics& g) 12 | { 13 | if (auto* main_panel = findParentComponentOfClass(); main_panel) { 14 | const auto& coordinates = main_panel->getCoordinates(); 15 | 16 | const auto total_bounds = getLocalBounds(); 17 | 18 | const auto toggle_left = total_bounds.withWidth(total_bounds.getWidth() / 2) 19 | .reduced(static_cast(coordinates.mSwitchMargin)); 20 | const auto toggle_right = toggle_left.translated(total_bounds.getWidth() / 2, 0); 21 | 22 | const auto bg_color_on = findColour(juce::Label::ColourIds::textColourId); 23 | const auto bg_color_off = findColour(juce::Label::ColourIds::backgroundColourId); 24 | 25 | g.setColour(bg_color_off); 26 | g.fillRoundedRectangle(total_bounds.toFloat(), coordinates.mKeyCorner); 27 | g.setColour(bg_color_on); 28 | g.fillRoundedRectangle(getToggleState() ? toggle_left.toFloat() : toggle_right.toFloat(), coordinates.mKeyCorner - 4.f); 29 | 30 | g.setFont(LnF::getDefaultFont(fontSize)); 31 | g.setColour(getToggleState() ? bg_color_off : bg_color_on); 32 | g.drawText(onText, toggle_left, juce::Justification::centred); 33 | g.setColour(getToggleState() ? bg_color_on : bg_color_off); 34 | g.drawText(offText, toggle_right, juce::Justification::centred); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/gui/widgets/TextSwitch.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "CustomToggleButton.h" 7 | 8 | namespace Gui 9 | { 10 | class TextSwitch : public Gui::CustomToggleButton 11 | { 12 | public: 13 | TextSwitch() = delete; 14 | explicit TextSwitch(const juce::String& name, juce::String inOnText, juce::String inOffText, float inFontSize = 20.f); 15 | 16 | void paint(juce::Graphics& g) override; 17 | 18 | void setOnText(const juce::String& inText) { onText = inText; } 19 | void setOffText(const juce::String& inText) { offText = inText; } 20 | private: 21 | juce::String onText; 22 | juce::String offText; 23 | float fontSize = 20.f; 24 | }; 25 | } -------------------------------------------------------------------------------- /src/params/ArpeggiatorParams.cpp: -------------------------------------------------------------------------------- 1 | #include "Params.h" 2 | 3 | namespace Params 4 | { 5 | 6 | void ArpeggiatorParams::addParams(juce::AudioProcessor& p) 7 | { 8 | p.addParameter(activated = new juce::AudioParameterBool(ParamIDs::arpeggiatorActivated, 9 | "Arpeggiator activated", 10 | false, 11 | juce::AudioParameterBoolAttributes().withLabel("Activated"))); 12 | p.addParameter(synced = new juce::AudioParameterBool(ParamIDs::arpeggiatorSync, 13 | "Arpeggiator Sync", 14 | false, 15 | juce::AudioParameterBoolAttributes().withLabel("Sync the arpeggiator with the host"))); 16 | p.addParameter(syncRate = new juce::AudioParameterInt(ParamIDs::arpeggiatorSyncRate, 17 | "Arpeggiator Rate Sync", 18 | 0, static_cast(Notes::divisions.size()) - 1, 2, 19 | juce::AudioParameterIntAttributes().withLabel("Rate"))); 20 | p.addParameter(rate = new juce::AudioParameterFloat(ParamIDs::arpeggiatorRate, 21 | "Arpeggiator Rate", 22 | juce::NormalisableRange(0.0f, 1.0f), 0.5f, 23 | juce::AudioParameterFloatAttributes().withLabel("Rate"))); 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/params/IntervalParam.cpp: -------------------------------------------------------------------------------- 1 | #include "Params.h" 2 | 3 | namespace Params 4 | { 5 | /********************************************* 6 | * IntervalParam 7 | *********************************************/ 8 | IntervalParam::IntervalParam(std::string name, std::string label, int i) 9 | : degree(i), noteName(std::move(name)), noteLabel(std::move(label)) 10 | {} 11 | 12 | void IntervalParam::addParam(juce::AudioProcessor& p) 13 | { 14 | const auto paramId = juce::String(noteName) + ParamIDs::noteInterval + juce::String(degree + 1); 15 | p.addParameter(interval = new juce::AudioParameterInt(paramId, paramId, -12, 12, 0, 16 | juce::AudioParameterIntAttributes().withLabel( 17 | "Interval " + juce::String(degree + 1) + " for " + 18 | noteLabel))); 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /src/params/MidiParams.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "Params.h" 4 | 5 | namespace Params 6 | { 7 | /******************************** 8 | * MidiParams 9 | ********************************/ 10 | 11 | void MidiParams::addParams(juce::AudioProcessor& p) 12 | { 13 | p.addParameter(inputChannel = new juce::AudioParameterInt(ParamIDs::inChannel, 14 | "Input Channel", 15 | 0, 16, 1, 16 | juce::AudioParameterIntAttributes().withLabel( 17 | "Input Channel"))); 18 | p.addParameter(outputChannel = new juce::AudioParameterInt(ParamIDs::outChannel, 19 | "Output Channel", 20 | 0, 16, 1, 21 | juce::AudioParameterIntAttributes().withLabel( 22 | "Output Channel"))); 23 | p.addParameter(octaveTranspose = new juce::AudioParameterInt(ParamIDs::octaveTranspose, 24 | "Transpose Octaves", 25 | -1, 4, 0, 26 | juce::AudioParameterIntAttributes().withLabel( 27 | "Transpose Octaves"))); 28 | } 29 | } -------------------------------------------------------------------------------- /src/params/NoteParam.cpp: -------------------------------------------------------------------------------- 1 | #include "Params.h" 2 | #include 3 | #include 4 | #include 5 | 6 | namespace Params 7 | { 8 | /*********************************************** 9 | * NoteParam 10 | ***********************************************/ 11 | NoteParam::NoteParam(const int index) 12 | : noteIndex(index), noteName(Notes::names[static_cast(index)]), noteLabel(Notes::labels[static_cast(index)]) 13 | { 14 | intervals.reserve(Notes::names.size()); 15 | for (int i = 0; i < Notes::count; i++) { 16 | intervals.emplace_back(new IntervalParam(noteName, noteLabel, i)); 17 | } 18 | } 19 | 20 | NoteParam::~NoteParam() 21 | { 22 | transpose->removeListener(this); 23 | for (auto& interval: intervals) { 24 | interval->interval->removeListener(this); 25 | } 26 | } 27 | 28 | void NoteParam::addParams(juce::AudioProcessor& p) 29 | { 30 | p.addParameter(mapNote = new juce::AudioParameterBool(juce::String(noteName) + ParamIDs::mapNote, 31 | "Map " + noteLabel, true, 32 | juce::AudioParameterBoolAttributes().withLabel( 33 | "Map " + noteLabel))); 34 | p.addParameter(transpose = new juce::AudioParameterInt(juce::String(noteName) + ParamIDs::noteTranspose, 35 | noteLabel + " transpose", 36 | -12, 12, 0, 37 | juce::AudioParameterIntAttributes().withLabel( 38 | "Transpose semitones"))); 39 | for (auto& interval: intervals) { 40 | interval->addParam(p); 41 | interval->interval->addListener(this); 42 | } 43 | 44 | mapNote->addListener(this); 45 | transpose->addListener(this); 46 | } 47 | 48 | void NoteParam::parameterValueChanged(int, float) 49 | { 50 | if (update != nullptr) { 51 | update(); 52 | } 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /src/params/NoteParams.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "Params.h" 3 | 4 | namespace Params 5 | { 6 | /*********************************************** 7 | * NoteParams 8 | ***********************************************/ 9 | NoteParams::NoteParams() 10 | { 11 | notes.reserve(Notes::count); 12 | for (int i = 0; i < Notes::count; i++) { 13 | notes.emplace_back(new NoteParam(i)); 14 | } 15 | } 16 | 17 | void NoteParams::addParams(juce::AudioProcessor& p) 18 | { 19 | for (auto& note: notes) { 20 | note->addParams(p); 21 | } 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /src/params/Params.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace ParamIDs 7 | { 8 | static const juce::String inChannel { "in_channel" }; 9 | static const juce::String outChannel { "out_channel" }; 10 | static const juce::String octaveTranspose { "octave_transpose" }; 11 | static const juce::String noteTranspose { "_noteTranspose" }; 12 | static const juce::String noteInterval { "_interval_" }; 13 | static const juce::String mapNote { "_mapNote" }; 14 | static const juce::String arpeggiatorActivated { "arpeggiator_activated" }; 15 | static const juce::String arpeggiatorSync { "arpeggiator_sync" }; 16 | static const juce::String arpeggiatorSyncRate { "arpeggiator_sync_rate" }; 17 | static const juce::String arpeggiatorRate { "arpeggiator_rate" }; 18 | } 19 | 20 | namespace Notes 21 | { 22 | static constexpr int count { 12 }; 23 | static constexpr std::array names { "C", "CS", "D", "DS", "E", "F", "FS", "G", "GS", "A", "AS", "B" }; 24 | static constexpr std::array labels { "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" }; 25 | 26 | static constexpr std::array whiteNotes { true, false, true, false, true, true, false, true, false, true, false, true }; 27 | 28 | struct Division 29 | { 30 | std::string label; 31 | double division; 32 | }; 33 | 34 | static const std::vector divisions { 35 | { "1/1", 4.0 }, 36 | { "1/2", 2.0 }, 37 | { "1/4.d", 1.5 }, 38 | { "1/4", 1.0 }, 39 | { "1/8d", 0.75 }, 40 | { "1/4.t", 2.0 / 3.0 }, 41 | { "1/8", 0.5 }, 42 | { "1/8.t", 1.0 / 3.0 }, 43 | { "1/16", 0.25 } 44 | }; 45 | } 46 | 47 | namespace Params 48 | { 49 | 50 | struct ParamHelper 51 | { 52 | static juce::String getParamID(juce::AudioProcessorParameter* param) 53 | { 54 | if (auto paramWithID = dynamic_cast(param)) 55 | return paramWithID->paramID; 56 | 57 | return param->getName(50); 58 | } 59 | }; 60 | 61 | /** 62 | * @brief Simple structure that contains the 3 MIDI parameters at the top of the plugin. 63 | */ 64 | struct MidiParams 65 | { 66 | MidiParams() = default; 67 | 68 | void addParams(juce::AudioProcessor& p); 69 | 70 | juce::AudioParameterInt* inputChannel = nullptr; 71 | juce::AudioParameterInt* outputChannel = nullptr; 72 | juce::AudioParameterInt* octaveTranspose = nullptr; 73 | 74 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiParams) 75 | }; 76 | 77 | /** 78 | * These are all the parameters related to the arpeggiator 79 | */ 80 | struct ArpeggiatorParams 81 | { 82 | ArpeggiatorParams() = default; 83 | 84 | void addParams(juce::AudioProcessor& p); 85 | 86 | juce::AudioParameterBool* activated = nullptr; 87 | juce::AudioParameterBool* synced = nullptr; 88 | juce::AudioParameterInt* syncRate = nullptr; 89 | juce::AudioParameterFloat* rate = nullptr; 90 | 91 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ArpeggiatorParams) 92 | }; 93 | 94 | /** 95 | * This is just an interval toggle for a specific note and degree. 96 | */ 97 | struct IntervalParam 98 | { 99 | IntervalParam() = delete; 100 | 101 | explicit IntervalParam(std::string name, std::string label, int i); 102 | 103 | void addParam(juce::AudioProcessor& p); 104 | 105 | juce::AudioParameterInt* interval = nullptr; 106 | int degree; 107 | std::string noteName; 108 | std::string noteLabel; 109 | 110 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (IntervalParam) 111 | }; 112 | 113 | /* 114 | This represents the structure for a note with its transposition and selected intervals. 115 | The update function is called on parameter changed to update the mappingNotes vector of the processor. 116 | */ 117 | struct NoteParam final : juce::AudioProcessorParameter::Listener 118 | { 119 | NoteParam() = delete; 120 | 121 | explicit NoteParam(int index); 122 | 123 | ~NoteParam() override; 124 | 125 | void addParams(juce::AudioProcessor& p); 126 | 127 | void parameterValueChanged(int, float) override; 128 | 129 | void parameterGestureChanged(int, bool) override 130 | {} 131 | 132 | int noteIndex; 133 | std::string noteName; 134 | std::string noteLabel; 135 | 136 | juce::AudioParameterBool* mapNote = nullptr; 137 | juce::AudioParameterInt* transpose = nullptr; 138 | std::vector> intervals; 139 | 140 | std::function update = nullptr; 141 | 142 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NoteParam) 143 | }; 144 | 145 | /* 146 | This structure contains all 12 note/chord couples. 147 | */ 148 | struct NoteParams 149 | { 150 | NoteParams(); 151 | 152 | void addParams(juce::AudioProcessor& p); 153 | 154 | std::vector> notes; 155 | 156 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NoteParams) 157 | }; 158 | 159 | } -------------------------------------------------------------------------------- /src/presets/PresetManager.cpp: -------------------------------------------------------------------------------- 1 | #include "PresetManager.h" 2 | #include "params/Params.h" 3 | #include "ProjectInfo.h" 4 | 5 | namespace PresetBrowser { 6 | 7 | const juce::String PresetManager::kPresetsExtension = "preset"; 8 | const juce::String PresetManager::kInitPreset = "Init"; 9 | 10 | PresetManager::PresetManager(juce::AudioProcessor& p) : 11 | processor(p), 12 | presetsPath(juce::File::getSpecialLocation( 13 | juce::File::SpecialLocationType::commonDocumentsDirectory) 14 | .getChildFile(ProjectInfo::companyName) 15 | .getChildFile(ProjectInfo::projectName)) 16 | { 17 | if (!presetsPath.exists()) { 18 | if (const auto result = presetsPath.createDirectory(); result.failed()) { 19 | DBG("Could not create preset directory: " + result.getErrorMessage()); 20 | jassertfalse; 21 | } 22 | } 23 | } 24 | 25 | bool PresetManager::savePreset(const juce::String& inPresetName) 26 | { 27 | if (inPresetName.isEmpty() || inPresetName == kInitPreset) { 28 | return false; 29 | } 30 | juce::XmlElement xml("preset"); 31 | 32 | auto* xml_params = new juce::XmlElement("params"); 33 | for (auto& param : processor.getParameters()) { 34 | xml_params->setAttribute(Params::ParamHelper::getParamID(param), param->getValue()); 35 | } 36 | 37 | xml.addChildElement(xml_params); 38 | const auto preset_file = presetsPath.getChildFile(inPresetName + "." + kPresetsExtension); 39 | if (!xml.writeTo(preset_file)) { 40 | DBG("Could not create preset file: " + preset_file.getFullPathName()); 41 | jassertfalse; 42 | return false; 43 | } 44 | currentPreset = inPresetName; 45 | return true; 46 | } 47 | 48 | void PresetManager::loadPreset(const juce::String& inPresetName) 49 | { 50 | if (inPresetName.isEmpty()) { 51 | return; 52 | } 53 | const auto preset_file = presetsPath.getChildFile(inPresetName + "." + kPresetsExtension); 54 | if (!preset_file.existsAsFile()) { 55 | DBG("Preset file " + preset_file.getFullPathName() + " does not exist"); 56 | jassertfalse; 57 | return; 58 | } 59 | 60 | juce::XmlDocument xml_document { preset_file }; 61 | auto xml = xml_document.getDocumentElementIfTagMatches("preset"); 62 | if (xml != nullptr) { 63 | auto params = xml->getChildByName("params"); 64 | if (params != nullptr) { 65 | for (auto& param : processor.getParameters()) { 66 | param->setValueNotifyingHost(static_cast(params->getDoubleAttribute(Params::ParamHelper::getParamID(param), param->getValue()))); 67 | } 68 | currentPreset = inPresetName; 69 | } 70 | } 71 | } 72 | 73 | void PresetManager::deletePreset(const juce::String& inPresetName) 74 | { 75 | if (inPresetName.isEmpty() || inPresetName == kInitPreset) { 76 | return; 77 | } 78 | 79 | const auto preset_file = presetsPath.getChildFile(inPresetName + "." + kPresetsExtension); 80 | if (!preset_file.existsAsFile()) { 81 | DBG("Preset file " + preset_file.getFullPathName() + " does not exist"); 82 | jassertfalse; 83 | return; 84 | } 85 | if (preset_file.deleteFile()) { 86 | DBG("Preset file " + preset_file.getFullPathName() + " deleted"); 87 | // Reload the default preset after the deletion. 88 | resetPreset(); 89 | } else { 90 | DBG("Could not delete preset file: " + preset_file.getFullPathName()); 91 | jassertfalse; 92 | } 93 | } 94 | 95 | void PresetManager::resetPreset() 96 | { 97 | for (auto& param : processor.getParameters()) { 98 | param->setValueNotifyingHost(param->getDefaultValue()); 99 | } 100 | currentPreset = kInitPreset; 101 | } 102 | 103 | juce::StringArray PresetManager::getAllPresets() const { 104 | juce::StringArray presets; 105 | const auto fileArray = presetsPath.findChildFiles( 106 | juce::File::TypesOfFileToFind::findFiles, false, "*." + kPresetsExtension); 107 | for (const auto& file : fileArray) { 108 | presets.add(file.getFileNameWithoutExtension()); 109 | } 110 | return presets; 111 | } 112 | } -------------------------------------------------------------------------------- /src/presets/PresetManager.h: -------------------------------------------------------------------------------- 1 | #ifndef MIDITRANSPOSER_PRESETMANAGER_H 2 | #define MIDITRANSPOSER_PRESETMANAGER_H 3 | 4 | #include 5 | #include 6 | 7 | namespace PresetBrowser { 8 | 9 | class PresetManager { 10 | public: 11 | PresetManager() = delete; 12 | explicit PresetManager(juce::AudioProcessor& p); 13 | 14 | /** 15 | * @brief Saves the current state of params in an XML file 16 | * @param inPresetName The name of the XML file 17 | */ 18 | bool savePreset(const juce::String& inPresetName); 19 | 20 | /** 21 | * @brief Loads an XML file and gets the param values to reset the audio processor. 22 | * @param inPresetName The name of the XML file to load. 23 | */ 24 | void loadPreset(const juce::String& inPresetName); 25 | 26 | /** 27 | * @brief Deletes a preset from the presets directory 28 | * @param inPresetName The name of the preset to delete 29 | */ 30 | void deletePreset(const juce::String& inPresetName); 31 | 32 | /** 33 | * @brief Resets all the parameters to their default values and set the current preset to Default. 34 | */ 35 | void resetPreset(); 36 | 37 | /** 38 | * @brief Reads the presets directory to get all the presets 39 | * @return An array of String to populate the combobox 40 | */ 41 | [[nodiscard]] juce::StringArray getAllPresets() const; 42 | 43 | [[nodiscard]] const juce::File& getPresetPath() const { return presetsPath; } 44 | [[nodiscard]] const juce::String& getCurrentPreset() const { return currentPreset; } 45 | void setCurrentPreset(const juce::String& inPreset) { currentPreset = inPreset; } 46 | 47 | private: 48 | juce::AudioProcessor& processor; 49 | 50 | /** 51 | * @brief The root folder of the presets 52 | */ 53 | juce::File presetsPath; 54 | 55 | /** 56 | * @brief The name of the currently loaded preset 57 | */ 58 | juce::String currentPreset; 59 | 60 | public: 61 | static const juce::String kPresetsExtension; 62 | static const juce::String kInitPreset; 63 | 64 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PresetManager) 65 | }; 66 | 67 | } 68 | 69 | #endif //MIDITRANSPOSER_PRESETMANAGER_H 70 | -------------------------------------------------------------------------------- /src/processor/MidiProcessor.cpp: -------------------------------------------------------------------------------- 1 | #include "MidiProcessor.h" 2 | 3 | /*================================================================ */ 4 | // PROCESS 5 | /*================================================================ */ 6 | void MidiProcessor::prepareToPlay(const double rate) 7 | { 8 | arp.reset(); 9 | arp.sampleRate = static_cast (rate); 10 | } 11 | 12 | void MidiProcessor::process(juce::MidiBuffer& midiMessages, const int numSamples, const juce::AudioPlayHead* playHead) 13 | { 14 | processedMidi.clear(); 15 | 16 | /** 17 | * This will filter notes on/off and keep other messages. 18 | * In arpeggiator mode it will just calculate the chord notes to play 19 | */ 20 | const auto inputChannel = midiParams.inputChannel->get(); 21 | for (const auto& metadata: midiMessages) { 22 | const auto& m = metadata.getMessage(); 23 | // Only notes on and off from input channel are processed, the rest is passed through. 24 | if (inputChannel == 0 || m.getChannel() == inputChannel) { 25 | if (m.isNoteOnOrOff()) { 26 | mapNote(m, metadata.samplePosition); 27 | } else { 28 | processedMidi.addEvent(m, metadata.samplePosition); 29 | } 30 | } 31 | } 32 | 33 | /** 34 | * If arpeggiator is activated, we need to calculate which note will be sent at which time. 35 | */ 36 | if (arpeggiatorParams.activated->get()) { 37 | processArpeggiator(numSamples, playHead); 38 | } 39 | 40 | midiMessages.swapWith(processedMidi); 41 | } 42 | 43 | /*================================================================ */ 44 | // CHORD MAPPING 45 | /*================================================================ */ 46 | /* 47 | There are many cases to take in account here. 48 | We want the input to be monophonic, so we need to know which was the last played note and 49 | if there are some notes still on to be played when the last one is released. 50 | */ 51 | void MidiProcessor::mapNote(const juce::MidiMessage& m, const int samplePosition) 52 | { 53 | // The output channel will be the original one or the one defined by the parameter knob. 54 | const auto outputChannel = midiParams.outputChannel->get(); 55 | const auto channel = outputChannel == 0 ? m.getChannel() : outputChannel; 56 | const NoteState noteState {m.getNoteNumber(), channel, m.getVelocity() }; 57 | if (m.isNoteOn()) { 58 | // Add the note to the vector of current notes played. 59 | currentInputNotesOn.push_back(noteState); 60 | 61 | // If the note changed, turn off the previous notes before adding the new ones. 62 | if (noteState.note != lastNoteOn.note && lastNoteOn.note > -1) { 63 | stopCurrentNotes(noteState.velocity, samplePosition); 64 | } 65 | 66 | // Play the received note. 67 | playMappedNotes(noteState, samplePosition); 68 | } else { 69 | // For every note off, remove the received note from the vector of current notes held. 70 | removeHeldNote(noteState.note); 71 | 72 | // Turn off the corresponding notes for the current note off if it's the same as the last played note. 73 | // Otherwise, it means the released note was not active, so we don't need to do anything (case of multiple notes held) 74 | if (noteState.note == lastNoteOn.note) { 75 | stopCurrentNotes(noteState.velocity, samplePosition); 76 | 77 | // If there were still some notes held, play the last one. 78 | if (!currentInputNotesOn.empty()) { 79 | playMappedNotes(currentInputNotesOn.back(), samplePosition); 80 | } else { 81 | // No note is currently played. 82 | lastNoteOn.reset(); 83 | if (arp.currentNote.note != -1) { 84 | processedMidi.addEvent(juce::MidiMessage::noteOff(arp.currentNote.channel, arp.currentNote.note, 85 | noteState.velocity), samplePosition); 86 | } 87 | arp.reset(); 88 | currentOutputNotesOn.clear(); 89 | } 90 | } 91 | } 92 | } 93 | 94 | void MidiProcessor::playMappedNotes(const NoteState& noteState, const int samplePosition) 95 | { 96 | // First recalculate the output notes vector. 97 | auto mappedNotes = getMappedNotes(noteState); 98 | currentOutputNotesOn.swap(mappedNotes); 99 | playCurrentNotes(samplePosition); 100 | lastNoteOn = noteState; 101 | arp.currentIndex = 0; 102 | } 103 | 104 | void MidiProcessor::playCurrentNotes(const int samplePosition) 105 | { 106 | // Nothing to do here if arpeggiator is on, it has its own loop to play the notes. 107 | if (arpeggiatorParams.activated->get()) { 108 | return; 109 | } 110 | 111 | for (const auto noteState: currentOutputNotesOn) { 112 | if (noteState.note >= 0 && noteState.note < 128) { 113 | processedMidi.addEvent(juce::MidiMessage::noteOn(noteState.channel, noteState.note, noteState.velocity), 114 | samplePosition); 115 | } 116 | } 117 | } 118 | 119 | void MidiProcessor::stopCurrentNotes(const juce::uint8 velocity, const int samplePosition) 120 | { 121 | if (arpeggiatorParams.activated->get()) { return; } 122 | 123 | for (const auto& noteState: currentOutputNotesOn) { 124 | processedMidi.addEvent(juce::MidiMessage::noteOff(noteState.channel, noteState.note, velocity), samplePosition); 125 | } 126 | } 127 | 128 | void MidiProcessor::removeHeldNote(const int note) 129 | { 130 | std::erase_if(currentInputNotesOn, 131 | [¬e](const auto& note_state) { 132 | return note_state.note == note; 133 | }); 134 | } 135 | 136 | std::vector MidiProcessor::getMappedNotes(const NoteState& noteState) const 137 | { 138 | std::vector mappedNotes; 139 | const int baseNote = noteState.note % 12; 140 | const auto octaveTranspose = midiParams.octaveTranspose->get(); 141 | 142 | // If there's an octave transpose, add the root note at its original height. 143 | if (octaveTranspose != 0) { 144 | mappedNotes.push_back(noteState); 145 | } 146 | 147 | // Then add all the notes from the mapping vector at the transposed height. 148 | for (const auto noteMapping: notesMapping[static_cast(baseNote)]) { 149 | mappedNotes.push_back({noteState.note + noteMapping + octaveTranspose * 12, noteState.channel, noteState.velocity}); 150 | } 151 | 152 | return mappedNotes; 153 | } 154 | 155 | /*================================================================ */ 156 | // ARPEGGIATOR 157 | /*================================================================ */ 158 | void MidiProcessor::processArpeggiator(const int numSamples, const juce::AudioPlayHead* playHead) 159 | { 160 | juce::Optional positionInfo; 161 | if (playHead != nullptr) { 162 | positionInfo = playHead->getPosition(); 163 | } 164 | 165 | // Update the arpeggiated notes if there's been an update in NoteParam listener 166 | // and the updated mapping is the currently played note. 167 | if (arp.noteUpdated > -1 && lastNoteOn.note % 12 == arp.noteUpdated) { 168 | auto mappedNotes = getMappedNotes(lastNoteOn); 169 | if (mappedNotes.size() < currentOutputNotesOn.size()) { 170 | arp.currentIndex = juce::jmin(arp.currentIndex, static_cast(mappedNotes.size()) - 1); 171 | } 172 | currentOutputNotesOn.swap(mappedNotes); 173 | arp.noteUpdated = -1; 174 | } 175 | 176 | // The synced arpeggiator is used only in a DAW context currently playing. 177 | // The note duration will still use the current BPM to be calculated anyway. 178 | if (arpeggiatorParams.synced->get() && positionInfo.hasValue() && positionInfo->getIsPlaying()) { 179 | arpeggiateSync(numSamples, *positionInfo); 180 | } else { 181 | arpeggiate(numSamples, *positionInfo); 182 | } 183 | } 184 | 185 | int MidiProcessor::getArpeggiatorNoteDuration(const juce::AudioPlayHead::PositionInfo& positionInfo) 186 | { 187 | if (arpeggiatorParams.synced->get() && positionInfo.getBpm().hasValue() && positionInfo.getBpm() != 0.) { 188 | arp.division = Notes::divisions[static_cast(arpeggiatorParams.syncRate->get())].division; 189 | auto bpm = positionInfo.getBpm().orFallback(60.); 190 | auto samplesPerBeat = arp.sampleRate / (bpm / 60.); 191 | return static_cast (std::ceil(samplesPerBeat * arp.division)); 192 | } 193 | return static_cast (std::ceil(arp.sampleRate * .1 * (.1 + (5. - 5. * arpeggiatorParams.rate->get())))); 194 | } 195 | 196 | // This is a simple arpeggiator function that does not care about transport timing because it's not in a playing context. 197 | void MidiProcessor::arpeggiate(const int numSamples, const juce::AudioPlayHead::PositionInfo& positionInfo) 198 | { 199 | auto noteDuration = getArpeggiatorNoteDuration(positionInfo); 200 | /* 201 | There are two possibilities here : 202 | - The number of samples of the current block is less than the number of samples of the note duration. 203 | => There can be only one note played during the block, and it will happen when note duration is before the next block compared 204 | to the elapsed time since last note was played. 205 | - The number of samples of the current block is greater than the number of samples of the note duration. 206 | => We loop through the number of samples and count elapsed time until no more note can be played. 207 | */ 208 | if (numSamples < noteDuration) { 209 | if (arp.time + numSamples >= noteDuration) { 210 | const auto offset = juce::jmax(0, juce::jmin(noteDuration - arp.time, numSamples - 1)); 211 | playArpeggiatorNote(offset); 212 | } 213 | arp.time = (arp.time + numSamples) % noteDuration; 214 | } else { 215 | while (arp.time < numSamples) { 216 | const auto offset = arp.time + noteDuration; 217 | if (offset < numSamples) { 218 | playArpeggiatorNote(offset); 219 | } 220 | arp.time += noteDuration; 221 | } 222 | arp.time = arp.time % numSamples; 223 | } 224 | } 225 | 226 | // The synced arpeggiator is used when there's a track playing in a DAW, and it requires a few calculations to snap to the grid. 227 | void MidiProcessor::arpeggiateSync(const int numSamples, const juce::AudioPlayHead::PositionInfo& positionInfo) 228 | { 229 | auto beatPosition = positionInfo.getPpqPosition(); 230 | auto samplesPerBeat = arp.sampleRate / (positionInfo.getBpm().orFallback(60.) / 60.); 231 | int offset { 0 }; 232 | 233 | while (offset < numSamples) { 234 | // Reset the position calculation if the division has changed. 235 | const auto lastDivision = Notes::divisions[static_cast(arpeggiatorParams.syncRate->get())].division; 236 | if (!juce::exactlyEqual(arp.division, lastDivision)) { 237 | // Update the current division from parameter 238 | arp.division = lastDivision; 239 | arp.nextBeatPosition = 0.; 240 | } 241 | 242 | // We need to get the current quarter note and see what's the next candidate position to snap to the current time division. 243 | if (arp.nextBeatPosition == 0.) { 244 | int nb_divisions = 1; 245 | while (arp.nextBeatPosition == 0.) { 246 | // For divisions greater than 1.0, we just snap to the next quarter note. 247 | const auto nextDivision = std::floor(*beatPosition) + (nb_divisions * std::min(1., arp.division)); 248 | if (nextDivision >= *beatPosition) { 249 | arp.nextBeatPosition = nextDivision; 250 | } 251 | 252 | nb_divisions++; 253 | } 254 | } 255 | 256 | // The next "snapping" time division occurs in this block! We need to calculate the offset here and play the note. 257 | offset = static_cast ((arp.nextBeatPosition - *beatPosition) * samplesPerBeat); 258 | if (offset < numSamples) { 259 | playArpeggiatorNote(offset); 260 | arp.nextBeatPosition += arp.division; 261 | } 262 | } 263 | } 264 | 265 | void MidiProcessor::playArpeggiatorNote(const int offset) 266 | { 267 | if (arp.currentNote.note > -1) { 268 | processedMidi.addEvent(juce::MidiMessage::noteOff(arp.currentNote.channel, arp.currentNote.note), offset); 269 | arp.currentNote.reset(); 270 | } 271 | 272 | if (!currentOutputNotesOn.empty()) { 273 | arp.currentNote = currentOutputNotesOn[static_cast(arp.currentIndex)]; 274 | processedMidi.addEvent( 275 | juce::MidiMessage::noteOn(arp.currentNote.channel, arp.currentNote.note, arp.currentNote.velocity), 276 | offset); 277 | arp.currentIndex = (arp.currentIndex + 1) % static_cast(currentOutputNotesOn.size()); 278 | } 279 | } 280 | 281 | /*================================================================ */ 282 | // PARAMETERS 283 | /*================================================================ */ 284 | void MidiProcessor::addParameters(juce::AudioProcessor& p) 285 | { 286 | midiParams.addParams(p); 287 | arpeggiatorParams.addParams(p); 288 | noteParams.addParams(p); 289 | 290 | // Each note param has its own listener and lambda function so only the corresponding note 291 | // is updated in the notesMapping vector. It also avoids testing paramID in parameterChanged method. 292 | for (auto& noteParam: noteParams.notes) { 293 | noteParam->update = [this, ¬eParam]() { updateNoteMapping(*noteParam); }; 294 | } 295 | 296 | initParameters(); 297 | } 298 | 299 | Params::MidiParams& MidiProcessor::getMidiParams() 300 | { 301 | return midiParams; 302 | } 303 | 304 | void MidiProcessor::initParameters() 305 | { 306 | for (auto& noteParam: noteParams.notes) { 307 | updateNoteMapping(*noteParam); 308 | } 309 | } 310 | 311 | // This method is called by the lambda associated to every noteParam when one of the note parameters is updated. 312 | void MidiProcessor::updateNoteMapping(const Params::NoteParam& inNoteParam) 313 | { 314 | std::set new_mapping; 315 | // There's a slider on each note defining a new interval to add. 316 | if (inNoteParam.mapNote->get()) { 317 | auto transpose = inNoteParam.transpose->get(); 318 | // Add the transposed value first, it's the root note. 319 | new_mapping.insert(transpose); 320 | 321 | // Then add the selected intervals. 322 | for (const auto& interval_param : inNoteParam.intervals) { 323 | new_mapping.insert(transpose + interval_param->interval->get()); 324 | } 325 | } else { 326 | new_mapping.insert(0); 327 | } 328 | 329 | // Finally, replace the old mapping. 330 | notesMapping[static_cast(inNoteParam.noteIndex)].swap(new_mapping); 331 | 332 | // Notify arpeggiator that there's been an update on this note. 333 | arp.noteUpdated = inNoteParam.noteIndex; 334 | } 335 | -------------------------------------------------------------------------------- /src/processor/MidiProcessor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "params/Params.h" 7 | 8 | class MidiProcessor 9 | { 10 | public: 11 | MidiProcessor() = default; 12 | 13 | void prepareToPlay(double rate); 14 | void process(juce::MidiBuffer& midiMessages, int numSamples, const juce::AudioPlayHead* playHead); 15 | void addParameters(juce::AudioProcessor& p); 16 | 17 | [[nodiscard]] int getLastNoteOn() const { return lastNoteOn.note; } 18 | 19 | Params::MidiParams& getMidiParams(); 20 | 21 | Params::NoteParams& getNoteParams() { return noteParams; } 22 | Params::ArpeggiatorParams& getArpeggiatorParams() { return arpeggiatorParams; } 23 | private: 24 | // Parameters declared in helper struct. The lambda will be called when a corresponding parameter is changed. 25 | Params::MidiParams midiParams; 26 | Params::NoteParams noteParams; 27 | Params::ArpeggiatorParams arpeggiatorParams; 28 | 29 | juce::MidiBuffer processedMidi; 30 | 31 | std::vector< std::set > notesMapping { 12, std::set({ 0 }) }; 32 | 33 | struct NoteState { 34 | int note { -1 }; 35 | int channel { 0 }; 36 | juce::uint8 velocity { 0 }; 37 | 38 | void reset() { note = -1; } 39 | }; 40 | NoteState lastNoteOn; 41 | std::vector currentInputNotesOn; 42 | std::vector currentOutputNotesOn; 43 | 44 | // Used to calculate the arpeggiator note positions. 45 | struct Arpeggiator 46 | { 47 | float sampleRate { 0.f }; 48 | int time { 0 }; 49 | double division { 0. }; 50 | double nextBeatPosition { 0. }; 51 | int currentIndex { 0 }; 52 | int noteUpdated { -1 }; 53 | NoteState currentNote; 54 | 55 | void reset() 56 | { 57 | time = 0; 58 | currentIndex = 0; 59 | noteUpdated = -1; 60 | division = 0.; 61 | nextBeatPosition = 0.; 62 | currentNote.reset(); 63 | } 64 | }; 65 | Arpeggiator arp; 66 | 67 | // ----------------------------------- 68 | // Process the input midi events 69 | void mapNote(const juce::MidiMessage& m, int samplePosition); 70 | void playMappedNotes(const NoteState& noteState, int samplePosition); 71 | [[nodiscard]] std::vector getMappedNotes(const NoteState& noteState) const; 72 | void playCurrentNotes(int samplePosition); 73 | void stopCurrentNotes(juce::uint8 velocity, int samplePosition); 74 | void removeHeldNote(int note); 75 | 76 | void processArpeggiator(int numSamples, const juce::AudioPlayHead* playHead); 77 | int getArpeggiatorNoteDuration(const juce::AudioPlayHead::PositionInfo& positionInfo); 78 | void arpeggiate(int numSamples, const juce::AudioPlayHead::PositionInfo& positionInfo); 79 | void arpeggiateSync(int numSamples, const juce::AudioPlayHead::PositionInfo& positionInfo); 80 | void playArpeggiatorNote(int offset); 81 | // ----------------------------------- 82 | 83 | // ----------------------------------- 84 | // Manage mapping values 85 | void initParameters(); 86 | void updateNoteMapping(const Params::NoteParam& inNoteParam); 87 | // ----------------------------------- 88 | 89 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiProcessor) 90 | }; 91 | -------------------------------------------------------------------------------- /src/processor/PluginProcessor.cpp: -------------------------------------------------------------------------------- 1 | #include "PluginProcessor.h" 2 | #include "gui/PluginEditor.h" 3 | 4 | //============================================================================== 5 | MidiTransposerAudioProcessor::MidiTransposerAudioProcessor() 6 | : juce::AudioProcessor (juce::AudioProcessor::BusesProperties().withInput("Input", juce::AudioChannelSet::mono(), true)), 7 | presetManager(*this) 8 | { 9 | // Add the parameters and listeners from the midi processor. 10 | midiProcessor.addParameters(*this); 11 | } 12 | 13 | MidiTransposerAudioProcessor::~MidiTransposerAudioProcessor() = default; 14 | 15 | //============================================================================== 16 | const juce::String MidiTransposerAudioProcessor::getName() const 17 | { 18 | return JucePlugin_Name; 19 | } 20 | 21 | bool MidiTransposerAudioProcessor::acceptsMidi() const { return true; } 22 | bool MidiTransposerAudioProcessor::producesMidi() const { return true; } 23 | bool MidiTransposerAudioProcessor::isMidiEffect() const { return true; } 24 | double MidiTransposerAudioProcessor::getTailLengthSeconds() const { return 0.0; } 25 | int MidiTransposerAudioProcessor::getNumPrograms() { return 1; } 26 | int MidiTransposerAudioProcessor::getCurrentProgram() { return 0; } 27 | void MidiTransposerAudioProcessor::setCurrentProgram (int) { } 28 | const juce::String MidiTransposerAudioProcessor::getProgramName (int) { return {}; } 29 | void MidiTransposerAudioProcessor::changeProgramName (int, const juce::String&) { } 30 | 31 | //============================================================================== 32 | void MidiTransposerAudioProcessor::releaseResources() {} 33 | bool MidiTransposerAudioProcessor::isBusesLayoutSupported (const juce::AudioProcessor::BusesLayout&) const { return true; } 34 | 35 | void MidiTransposerAudioProcessor::prepareToPlay (double sampleRate, int) 36 | { 37 | midiProcessor.prepareToPlay(sampleRate); 38 | } 39 | 40 | void MidiTransposerAudioProcessor::processBlock (juce::AudioBuffer& buffer, juce::MidiBuffer& midiMessages) 41 | { 42 | buffer.clear(); 43 | // The real processing is made in the MidiProcessor class. 44 | midiProcessor.process(midiMessages, buffer.getNumSamples(), getPlayHead()); 45 | } 46 | 47 | //============================================================================== 48 | bool MidiTransposerAudioProcessor::hasEditor() const { return true; } 49 | 50 | juce::AudioProcessorEditor* MidiTransposerAudioProcessor::createEditor() 51 | { 52 | return new MidiTransposerAudioProcessorEditor(*this); 53 | } 54 | 55 | //============================================================================== 56 | void MidiTransposerAudioProcessor::getStateInformation (juce::MemoryBlock& destData) 57 | { 58 | juce::XmlElement xml("PluginState"); 59 | 60 | auto* xml_params = new juce::XmlElement("params"); 61 | for (const auto& param : getParameters()) { 62 | xml_params->setAttribute(Params::ParamHelper::getParamID(param), param->getValue()); 63 | } 64 | // Store the name of the current preset. 65 | uiSettings.presetName = presetManager.getCurrentPreset(); 66 | 67 | xml.addChildElement(xml_params); 68 | xml.addChildElement(uiSettings.getXml()); 69 | 70 | copyXmlToBinary(xml, destData); 71 | } 72 | 73 | void MidiTransposerAudioProcessor::setStateInformation (const void* data, int sizeInBytes) 74 | { 75 | auto xml = getXmlFromBinary(data, sizeInBytes); 76 | 77 | if (xml != nullptr) { 78 | auto params = xml->getChildByName("params"); 79 | if (params != nullptr) { 80 | for (auto& param: getParameters()) { 81 | param->setValueNotifyingHost( 82 | static_cast(params->getDoubleAttribute(Params::ParamHelper::getParamID(param), 83 | param->getValue()))); 84 | } 85 | } 86 | 87 | uiSettings = Gui::UISettings(xml->getChildByName("UISettings")); 88 | presetManager.setCurrentPreset(uiSettings.presetName); 89 | } 90 | } 91 | 92 | void MidiTransposerAudioProcessor::saveEditorSize(int w, int h) 93 | { 94 | uiSettings.width = w; 95 | uiSettings.height = h; 96 | } 97 | 98 | //============================================================================== 99 | // This creates new instances of the plugin.. 100 | juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() 101 | { 102 | return new MidiTransposerAudioProcessor(); 103 | } 104 | -------------------------------------------------------------------------------- /src/processor/PluginProcessor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "processor/MidiProcessor.h" 6 | #include "gui/UISettings.h" 7 | #include "presets/PresetManager.h" 8 | 9 | /** 10 | * 11 | */ 12 | class MidiTransposerAudioProcessor final : public juce::AudioProcessor 13 | { 14 | public: 15 | //============================================================================== 16 | MidiTransposerAudioProcessor(); 17 | ~MidiTransposerAudioProcessor() override; 18 | 19 | //============================================================================== 20 | void prepareToPlay (double sampleRate, int samplesPerBlock) override; 21 | void releaseResources() override; 22 | 23 | #ifndef JucePlugin_PreferredChannelConfigurations 24 | bool isBusesLayoutSupported (const juce::AudioProcessor::BusesLayout& layouts) const override; 25 | #endif 26 | 27 | void processBlock (juce::AudioBuffer&, juce::MidiBuffer&) override; 28 | 29 | //============================================================================== 30 | juce::AudioProcessorEditor* createEditor() override; 31 | bool hasEditor() const override; 32 | 33 | //============================================================================== 34 | const juce::String getName() const override; 35 | 36 | bool acceptsMidi() const override; 37 | bool producesMidi() const override; 38 | bool isMidiEffect() const override; 39 | double getTailLengthSeconds() const override; 40 | 41 | //============================================================================== 42 | int getNumPrograms() override; 43 | int getCurrentProgram() override; 44 | void setCurrentProgram (int index) override; 45 | const juce::String getProgramName (int index) override; 46 | void changeProgramName (int index, const juce::String& newName) override; 47 | 48 | //============================================================================== 49 | void getStateInformation (juce::MemoryBlock& destData) override; 50 | void setStateInformation (const void* data, int sizeInBytes) override; 51 | 52 | //============================================================================== 53 | MidiProcessor& getMidiProcessor() { return midiProcessor; } 54 | Gui::UISettings& getUISettings() { return uiSettings; } 55 | PresetBrowser::PresetManager& getPresetManager() { return presetManager; } 56 | void saveEditorSize(int w, int h); 57 | private: 58 | Gui::UISettings uiSettings; 59 | PresetBrowser::PresetManager presetManager; 60 | MidiProcessor midiProcessor; 61 | 62 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiTransposerAudioProcessor) 63 | }; 64 | --------------------------------------------------------------------------------