From 7b763c0985c2c8c0f22ed04762a8da2c2958ed5e Mon Sep 17 00:00:00 2001 From: Tobias Hienzsch Date: Mon, 12 Feb 2024 08:18:43 +0100 Subject: [PATCH] Add plugin subproject --- CMakeLists.txt | 6 ++ src/plugin/CMakeLists.txt | 66 +++++++++++++ src/plugin/src/PluginEditor.cpp | 17 ++++ src/plugin/src/PluginEditor.hpp | 17 ++++ src/plugin/src/PluginParameter.cpp | 21 +++++ src/plugin/src/PluginParameter.hpp | 22 +++++ src/plugin/src/PluginProcessor.cpp | 143 +++++++++++++++++++++++++++++ src/plugin/src/PluginProcessor.hpp | 62 +++++++++++++ 8 files changed, 354 insertions(+) create mode 100644 src/plugin/CMakeLists.txt create mode 100644 src/plugin/src/PluginEditor.cpp create mode 100644 src/plugin/src/PluginEditor.hpp create mode 100644 src/plugin/src/PluginParameter.cpp create mode 100644 src/plugin/src/PluginParameter.hpp create mode 100644 src/plugin/src/PluginProcessor.cpp create mode 100644 src/plugin/src/PluginProcessor.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3c3d883..bea1d14 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,8 @@ cmake_minimum_required(VERSION 3.23...3.27) file(STRINGS VERSION CURRENT_VERSION) project(grit-eurorack-dev VERSION ${CURRENT_VERSION} LANGUAGES C CXX) +option(GRITWAVE_EURORACK_ENABLE_PLUGIN "Build plugin (development tool)" OFF) + find_program(CCACHE ccache) if (CCACHE) set(CMAKE_C_COMPILER_LAUNCHER ${CCACHE}) @@ -75,4 +77,8 @@ if(NOT CMAKE_CROSSCOMPILING) "lib/grit/math/static_lookup_table_test.cpp" "lib/grit/unit/decibel_test.cpp" ) + + if(GRITWAVE_EURORACK_ENABLE_PLUGIN) + add_subdirectory(src/plugin) + endif() endif() diff --git a/src/plugin/CMakeLists.txt b/src/plugin/CMakeLists.txt new file mode 100644 index 0000000..a94c74c --- /dev/null +++ b/src/plugin/CMakeLists.txt @@ -0,0 +1,66 @@ +project(gritwave-gritrack VERSION ${CMAKE_PROJECT_VERSION}) + +FetchContent_Declare(JUCE GIT_REPOSITORY "https://github.com/juce-framework/JUCE" GIT_TAG "develop" GIT_SHALLOW TRUE) +FetchContent_MakeAvailable(JUCE) + +set_directory_properties(PROPERTIES JUCE_COMPANY_COPYRIGHT "Copyright 2024 gritwave. All rights reserved.") +set_directory_properties(PROPERTIES JUCE_COMPANY_NAME "gritwave") +set_directory_properties(PROPERTIES JUCE_VST3_COPY_DIR "${CMAKE_BINARY_DIR}/VST3") +set_directory_properties(PROPERTIES JUCE_AU_COPY_DIR "${CMAKE_BINARY_DIR}/AU") + +juce_add_plugin(Gritrack + PRODUCT_NAME "Gritrack" # The name of the final executable, which can differ from the target name + BUNDLE_ID "com.gritwave.gritrack" # Should uniquely identify this target + PLUGIN_CODE Grtr # A unique four-character plugin id with exactly one upper-case character + PLUGIN_MANUFACTURER_CODE Grtw # A four-character manufacturer id with at least one upper-case character + FORMATS AU VST3 Standalone # The formats to build. Other valid formats are: AAX Unity VST AU AUv3 + HARDENED_RUNTIME_ENABLED TRUE # Enables macOS' hardened runtime for this target. Required for notarisation. + COPY_PLUGIN_AFTER_BUILD TRUE # Should the plugin be installed to a default location after building? + IS_SYNTH FALSE # Is this a synth or an effect? + NEEDS_MIDI_INPUT FALSE # Does the plugin need midi input? + NEEDS_MIDI_OUTPUT FALSE # Does the plugin need midi output? + IS_MIDI_EFFECT FALSE # Is this plugin a MIDI effect? + EDITOR_WANTS_KEYBOARD_FOCUS FALSE # Does the editor need keyboard focus? +) + +set(Gritrack_Sources + "src/PluginEditor.hpp" + "src/PluginEditor.cpp" + "src/PluginParameter.hpp" + "src/PluginParameter.cpp" + "src/PluginProcessor.hpp" + "src/PluginProcessor.cpp" +) + +target_sources(Gritrack PRIVATE ${Gritrack_Sources}) +set_target_properties(Gritrack PROPERTIES UNITY_BUILD ON UNITY_BUILD_MODE GROUP) +set_source_files_properties(Gritrack ${Gritrack_Sources} PROPERTIES UNITY_GROUP "src") + +target_compile_definitions(Gritrack + PUBLIC + JUCE_WEB_BROWSER=0 + JUCE_USE_CURL=0 + JUCE_VST3_CAN_REPLACE_VST2=0 + JUCE_USE_MP3AUDIOFORMAT=0 + JUCE_DISPLAY_SPLASH_SCREEN=0 + JUCE_MODAL_LOOPS_PERMITTED=1 + JUCE_REPORT_APP_USAGE=0 + JUCE_DISABLE_JUCE_VERSION_PRINTING=1 +) + +target_include_directories(Gritrack + PUBLIC + ${PROJECT_SOURCE_DIR}/src +) + +target_link_libraries(Gritrack + PRIVATE + juce::juce_audio_utils + juce::juce_dsp + + PUBLIC + gritwave::eurorack + juce::juce_recommended_config_flags + # juce::juce_recommended_lto_flags + # juce::juce_recommended_warning_flags +) diff --git a/src/plugin/src/PluginEditor.cpp b/src/plugin/src/PluginEditor.cpp new file mode 100644 index 0000000..c60249e --- /dev/null +++ b/src/plugin/src/PluginEditor.cpp @@ -0,0 +1,17 @@ +#include "PluginEditor.hpp" + +PluginEditor::PluginEditor(PluginProcessor& p) : AudioProcessorEditor(&p) +{ + _tooltipWindow->setMillisecondsBeforeTipAppears(750); + setSize(600, 400); +} + +PluginEditor::~PluginEditor() noexcept { setLookAndFeel(nullptr); } + +auto PluginEditor::paint(juce::Graphics& g) -> void +{ + // background + g.fillAll(getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId)); +} + +auto PluginEditor::resized() -> void {} diff --git a/src/plugin/src/PluginEditor.hpp b/src/plugin/src/PluginEditor.hpp new file mode 100644 index 0000000..7f33694 --- /dev/null +++ b/src/plugin/src/PluginEditor.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "PluginProcessor.hpp" + +#include + +struct PluginEditor final : juce::AudioProcessorEditor +{ + explicit PluginEditor(PluginProcessor& p); + ~PluginEditor() noexcept override; + + auto paint(juce::Graphics& g) -> void override; + auto resized() -> void override; + +private: + juce::SharedResourcePointer _tooltipWindow; +}; diff --git a/src/plugin/src/PluginParameter.cpp b/src/plugin/src/PluginParameter.cpp new file mode 100644 index 0000000..30564b3 --- /dev/null +++ b/src/plugin/src/PluginParameter.cpp @@ -0,0 +1,21 @@ +#include "PluginParameter.hpp" + +namespace gritrack { + +auto createParameters() -> juce::AudioProcessorValueTreeState::ParameterLayout +{ + auto const normalized = juce::NormalisableRange(0.0F, 1.0F); + + return { + std::make_unique(juce::ParameterID{ParamID::cv1, 1}, "CV-1", normalized, 0.0F), + std::make_unique(juce::ParameterID{ParamID::cv2, 1}, "CV-2", normalized, 0.0F), + std::make_unique(juce::ParameterID{ParamID::cv3, 1}, "CV-3", normalized, 0.0F), + std::make_unique(juce::ParameterID{ParamID::cv4, 1}, "CV-4", normalized, 0.0F), + std::make_unique(juce::ParameterID{ParamID::cv5, 1}, "CV-5", normalized, 0.0F), + std::make_unique(juce::ParameterID{ParamID::cv6, 1}, "CV-6", normalized, 0.0F), + std::make_unique(juce::ParameterID{ParamID::cv7, 1}, "CV-7", normalized, 0.0F), + std::make_unique(juce::ParameterID{ParamID::cv8, 1}, "CV-8", normalized, 0.0F), + }; +} + +} // namespace gritrack diff --git a/src/plugin/src/PluginParameter.hpp b/src/plugin/src/PluginParameter.hpp new file mode 100644 index 0000000..c61861e --- /dev/null +++ b/src/plugin/src/PluginParameter.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +namespace gritrack { + +struct ParamID +{ + static constexpr char const* cv1 = "cv1"; + static constexpr char const* cv2 = "cv2"; + static constexpr char const* cv3 = "cv3"; + static constexpr char const* cv4 = "cv4"; + static constexpr char const* cv5 = "cv5"; + static constexpr char const* cv6 = "cv6"; + static constexpr char const* cv7 = "cv7"; + static constexpr char const* cv8 = "cv8"; +}; + +auto createParameters() -> juce::AudioProcessorValueTreeState::ParameterLayout; + +} // namespace gritrack diff --git a/src/plugin/src/PluginProcessor.cpp b/src/plugin/src/PluginProcessor.cpp new file mode 100644 index 0000000..7ca35dd --- /dev/null +++ b/src/plugin/src/PluginProcessor.cpp @@ -0,0 +1,143 @@ + +#include "PluginProcessor.hpp" + +#include "PluginEditor.hpp" +#include "PluginParameter.hpp" + +PluginProcessor::PluginProcessor() + : AudioProcessor(BusesProperties() + .withInput("Input", juce::AudioChannelSet::stereo(), true) + .withOutput("Output", juce::AudioChannelSet::stereo(), true)) + , _valueTree{*this, nullptr, juce::Identifier("Gritwave"), gritrack::createParameters()} + , _cv1{*dynamic_cast(_valueTree.getParameter(gritrack::ParamID::cv1))} + , _cv2{*dynamic_cast(_valueTree.getParameter(gritrack::ParamID::cv2))} + , _cv3{*dynamic_cast(_valueTree.getParameter(gritrack::ParamID::cv3))} + , _cv4{*dynamic_cast(_valueTree.getParameter(gritrack::ParamID::cv4))} + , _cv5{*dynamic_cast(_valueTree.getParameter(gritrack::ParamID::cv5))} + , _cv6{*dynamic_cast(_valueTree.getParameter(gritrack::ParamID::cv6))} + , _cv7{*dynamic_cast(_valueTree.getParameter(gritrack::ParamID::cv7))} + , _cv8{*dynamic_cast(_valueTree.getParameter(gritrack::ParamID::cv8))} +{} + +PluginProcessor::~PluginProcessor() = default; + +auto PluginProcessor::getName() const -> juce::String const { return JucePlugin_Name; } + +auto PluginProcessor::acceptsMidi() const -> bool { return false; } + +auto PluginProcessor::producesMidi() const -> bool { return false; } + +auto PluginProcessor::isMidiEffect() const -> bool { return false; } + +auto PluginProcessor::getTailLengthSeconds() const -> double { return 0.0; } + +auto PluginProcessor::getNumPrograms() -> int { return 1; } + +auto PluginProcessor::getCurrentProgram() -> int { return 0; } + +auto PluginProcessor::setCurrentProgram(int index) -> void { juce::ignoreUnused(index); } + +auto PluginProcessor::getProgramName(int index) -> juce::String const +{ + juce::ignoreUnused(index); + return {}; +} + +auto PluginProcessor::changeProgramName(int index, juce::String const& newName) -> void +{ + juce::ignoreUnused(index, newName); +} + +auto PluginProcessor::prepareToPlay(double sampleRate, int samplesPerBlock) -> void +{ + _hades = std::make_unique(); + _hades->prepare(sampleRate, static_cast(samplesPerBlock)); + + _buffer.resize(2U * static_cast(samplesPerBlock)); +} + +auto PluginProcessor::releaseResources() -> void {} + +auto PluginProcessor::isBusesLayoutSupported(BusesLayout const& layouts) const -> bool +{ + if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo()) { + return false; + } + + if (layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet()) { + return false; + } + + return true; +} + +void PluginProcessor::processBlock(juce::AudioBuffer& buffer, juce::MidiBuffer& midiMessages) +{ + juce::ignoreUnused(midiMessages); + juce::ScopedNoDenormals const noDenormals; + + for (auto i = getTotalNumInputChannels(); i < getTotalNumOutputChannels(); ++i) { + buffer.clear(i, 0, buffer.getNumSamples()); + } + + auto io = grit::StereoBlock{_buffer.data(), static_cast(buffer.getNumSamples())}; + for (auto i = 0; i < buffer.getNumSamples(); ++i) { + io(0, i) = buffer.getSample(0, i); + io(1, i) = buffer.getSample(1, i); + } + + auto const controls = grit::Hades::ControlInput{ + .textureKnob = _cv1, + .morphKnob = _cv2, + .ampKnob = _cv3, + .compressorKnob = _cv4, + .morphCV = _cv5, + .sideChainCV = _cv6, + .attackCV = _cv7, + .releaseCV = _cv8, + .gate1 = false, + .gate2 = false, + }; + + auto const cv = _hades->process(io, controls); + juce::ignoreUnused(cv); + + for (auto i = 0; i < buffer.getNumSamples(); ++i) { + buffer.setSample(0, i, io(0, i)); + buffer.setSample(1, i, io(1, i)); + } +} + +auto PluginProcessor::parameterChanged(juce::String const& parameterID, float newValue) -> void +{ + juce::ignoreUnused(newValue, parameterID); +} + +auto PluginProcessor::hasEditor() const -> bool { return true; } + +auto PluginProcessor::createEditor() -> juce::AudioProcessorEditor* +{ + return new juce::GenericAudioProcessorEditor(*this); +} + +auto PluginProcessor::getStateInformation(juce::MemoryBlock& destData) -> void +{ + juce::MemoryOutputStream stream(destData, false); + _valueTree.state.writeToStream(stream); +} + +auto PluginProcessor::setStateInformation(void const* data, int sizeInBytes) -> void +{ + juce::ValueTree const tree = juce::ValueTree::readFromData(data, static_cast(sizeInBytes)); + jassert(tree.isValid()); + if (tree.isValid()) { + _valueTree.state = tree; + } +} + +auto PluginProcessor::getState() noexcept -> juce::AudioProcessorValueTreeState& { return _valueTree; } + +auto PluginProcessor::getState() const noexcept -> juce::AudioProcessorValueTreeState const& { return _valueTree; } + +// This creates new instances of the plugin.. +auto JUCE_CALLTYPE createPluginFilter() -> juce::AudioProcessor* { return new PluginProcessor(); } diff --git a/src/plugin/src/PluginProcessor.hpp b/src/plugin/src/PluginProcessor.hpp new file mode 100644 index 0000000..ec6c482 --- /dev/null +++ b/src/plugin/src/PluginProcessor.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include + +#include +#include +#include + +struct PluginProcessor final + : juce::AudioProcessor + , juce::AudioProcessorValueTreeState::Listener +{ + PluginProcessor(); + ~PluginProcessor() override; + + auto getName() const -> juce::String const override; + + auto prepareToPlay(double sampleRate, int samplesPerBlock) -> void override; + auto processBlock(juce::AudioBuffer& buffer, juce::MidiBuffer& midiMessages) -> void override; + using juce::AudioProcessor::processBlock; + auto releaseResources() -> void override; + + auto isBusesLayoutSupported(BusesLayout const& layouts) const -> bool override; + + auto createEditor() -> juce::AudioProcessorEditor* override; + auto hasEditor() const -> bool override; + + auto acceptsMidi() const -> bool override; + auto producesMidi() const -> bool override; + auto isMidiEffect() const -> bool override; + auto getTailLengthSeconds() const -> double override; + + auto getNumPrograms() -> int override; + auto getCurrentProgram() -> int override; + void setCurrentProgram(int index) override; + auto getProgramName(int index) -> juce::String const override; + void changeProgramName(int index, juce::String const& newName) override; + + auto getStateInformation(juce::MemoryBlock& destData) -> void override; + auto setStateInformation(void const* data, int sizeInBytes) -> void override; + + auto parameterChanged(juce::String const& parameterID, float newValue) -> void override; + + auto getState() noexcept -> juce::AudioProcessorValueTreeState&; + auto getState() const noexcept -> juce::AudioProcessorValueTreeState const&; + +private: + juce::UndoManager _undoManager{}; + juce::AudioProcessorValueTreeState _valueTree; + + std::vector _buffer{}; + std::unique_ptr _hades{nullptr}; + + juce::AudioParameterFloat& _cv1; + juce::AudioParameterFloat& _cv2; + juce::AudioParameterFloat& _cv3; + juce::AudioParameterFloat& _cv4; + juce::AudioParameterFloat& _cv5; + juce::AudioParameterFloat& _cv6; + juce::AudioParameterFloat& _cv7; + juce::AudioParameterFloat& _cv8; +};