gearmulator

Emulation of classic VA synths of the late 90s/2000s that are based on Motorola 56300 family DSPs
Log | Files | Refs | Submodules | README | LICENSE

commit aecee505d8fb9a374d108b46289fc772acfab8af
parent b8ca98fab871ff1caa91c11a3a0b7a438bab892c
Author: dsp56300 <dsp56300@users.noreply.github.com>
Date:   Sun, 15 May 2022 02:24:51 +0200

begin to split virus controller & parameter into generic and Virus specific parts

Diffstat:
Msource/jucePlugin/VirusController.cpp | 179++++++++++---------------------------------------------------------------------
Msource/jucePlugin/VirusController.h | 60++++++++++--------------------------------------------------
Msource/jucePlugin/VirusParameter.cpp | 135+++++++++++--------------------------------------------------------------------
Msource/jucePlugin/VirusParameter.h | 78+++++-------------------------------------------------------------------------
Msource/jucePlugin/VirusParameterBinding.cpp | 6+++---
Msource/jucePluginLib/CMakeLists.txt | 1+
Msource/jucePluginLib/controller.cpp | 154+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msource/jucePluginLib/controller.h | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginLib/parameter.cpp | 126+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginLib/parameter.h | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
10 files changed, 470 insertions(+), 400 deletions(-)

diff --git a/source/jucePlugin/VirusController.cpp b/source/jucePlugin/VirusController.cpp @@ -12,9 +12,12 @@ namespace Virus static constexpr uint8_t kSysExStart[] = {0xf0, 0x00, 0x20, 0x33, 0x01}; static constexpr auto kHeaderWithMsgCodeLen = 7; - Controller::Controller(AudioPluginAudioProcessor &p, unsigned char deviceId) : m_processor(p), m_deviceId(deviceId), m_descriptions(BinaryData::parameterDescriptions_C_json) + Controller::Controller(AudioPluginAudioProcessor &p, unsigned char deviceId) : pluginLib::Controller(BinaryData::parameterDescriptions_C_json), m_processor(p), m_deviceId(deviceId) { - assert(m_descriptions.getDescriptions().size() == Param_Count && "size of enum must match size of parameter descriptions"); +// assert(m_descriptions.getDescriptions().size() == Param_Count && "size of enum must match size of parameter descriptions"); + + registerParams(p); + juce::PropertiesFile::Options opts; opts.applicationName = "DSP56300 Emulator"; opts.filenameSuffix = ".settings"; @@ -22,7 +25,6 @@ namespace Virus opts.osxLibrarySubFolder = "Application Support/DSP56300 Emulator"; m_config = new juce::PropertiesFile(opts); - registerParams(); // add lambda to enforce updating patches when virus switch from/to multi/single. const auto& params = findSynthParam(0, 0x72, 0x7a); for (const auto& parameter : params) @@ -52,103 +54,6 @@ namespace Virus delete m_config; } - void Controller::registerParams() - { - // 16 parts * 3 pages * 128 params - // TODO: not register internal/unused params? - auto globalParams = std::make_unique<juce::AudioProcessorParameterGroup>("global", "Global", "|"); - - std::map<ParamIndex, int> knownParameterIndices; - - for (uint8_t part = 0; part < 16; part++) - { - m_paramsByParamType[part].reserve(m_descriptions.getDescriptions().size()); - - const auto partNumber = juce::String(part + 1); - auto group = - std::make_unique<juce::AudioProcessorParameterGroup>("ch" + partNumber, "Ch " + partNumber, "|"); - - uint32_t parameterDescIndex = 0; - for (const auto& desc : m_descriptions.getDescriptions()) - { - ++parameterDescIndex; - - const ParamIndex idx = {static_cast<uint8_t>(desc.page), part, desc.index}; - - int uid = 0; - - auto itKnownParamIdx = knownParameterIndices.find(idx); - - if(itKnownParamIdx == knownParameterIndices.end()) - knownParameterIndices.insert(std::make_pair(idx, 0)); - else - uid = ++itKnownParamIdx->second; - - auto p = std::make_unique<Parameter>(*this, desc, part, uid); - - if(uid > 0) - { - const auto& existingParams = findSynthParam(idx); - - for (auto& existingParam : existingParams) - existingParam->addLinkedParameter(p.get()); - } - - m_paramsByParamType[part].push_back(p.get()); - - const bool isNonPartExclusive = (desc.classFlags & (int)pluginLib::ParameterClass::Global) || (desc.classFlags & (int)pluginLib::ParameterClass::NonPartSensitive); - if (isNonPartExclusive) - { - if (part != 0) - continue; // only register on first part! - } - if (p->getDescription().isPublic) - { - // lifecycle managed by Juce - - auto itExisting = m_synthParams.find(idx); - if (itExisting != m_synthParams.end()) - { - itExisting->second.push_back(p.get()); - } - else - { - ParameterList params; - params.emplace_back(p.get()); - m_synthParams.insert(std::make_pair(idx, std::move(params))); - } - - if (isNonPartExclusive) - { - jassert(part == 0); - globalParams->addChild(std::move(p)); - } - else - group->addChild(std::move(p)); - } - else - { - // lifecycle handled by us - - auto itExisting = m_synthInternalParams.find(idx); - if (itExisting != m_synthInternalParams.end()) - { - itExisting->second.push_back(p.get()); - } - else - { - ParameterList params; - params.emplace_back(p.get()); - m_synthInternalParams.insert(std::make_pair(idx, std::move(params))); - } - m_synthInternalParamList.emplace_back(std::move(p)); - } - } - m_processor.addParameterGroup(std::move(group)); - } - m_processor.addParameterGroup(std::move(globalParams)); - } - void Controller::parseMessage(const SysEx &msg) { if (msg.size() < 8) @@ -199,31 +104,6 @@ namespace Virus } } - const Controller::ParameterList& Controller::findSynthParam(const uint8_t _part, const uint8_t _page, const uint8_t _paramIndex) - { - const ParamIndex paramIndex{ _page, _part, _paramIndex }; - - return findSynthParam(paramIndex); - } - - const Controller::ParameterList& Controller::findSynthParam(const ParamIndex& _paramIndex) - { - const auto it = m_synthParams.find(_paramIndex); - - if (it != m_synthParams.end()) - return it->second; - - const auto iti = m_synthInternalParams.find(_paramIndex); - - if (iti == m_synthInternalParams.end()) - { - static ParameterList empty; - return empty; - } - - return iti->second; - } - juce::Value* Controller::getParamValue(uint8_t ch, uint8_t bank, uint8_t paramIndex) { const auto& params = findSynthParam(ch, static_cast<uint8_t>(virusLib::PAGE_A + bank), paramIndex); @@ -235,28 +115,6 @@ namespace Virus return &params.front()->getValueObject(); } - juce::Value* Controller::getParamValue(const ParameterType _param) - { - const auto res = getParameter(_param); - return res ? &res->getValueObject() : nullptr; - } - - Parameter* Controller::getParameter(const ParameterType _param) const - { - return getParameter(_param, 0); - } - - Parameter* Controller::getParameter(const ParameterType _param, const uint8_t _part) const - { - if (_part >= m_paramsByParamType.size()) - return nullptr; - - if (_param >= m_paramsByParamType[_part].size()) - return nullptr; - - return m_paramsByParamType[_part][_param]; - } - void Controller::parseParamChange(const SysEx &msg) { constexpr auto pos = kHeaderWithMsgCodeLen - 1; @@ -525,7 +383,7 @@ namespace Virus return sum; } - template <typename T> juce::String Controller::parseAsciiText(const T &msg, const int start) const + template <typename T> juce::String Controller::parseAsciiText(const T &msg, const int start) { char text[kNameLength + 1]; text[kNameLength] = 0; // termination @@ -534,7 +392,7 @@ namespace Virus return {text}; } - void Controller::printMessage(const SysEx &msg) const + void Controller::printMessage(const SysEx &msg) { std::stringstream ss; for (auto &m : msg) @@ -546,14 +404,10 @@ namespace Virus ParameterType Controller::getParameterTypeByName(const std::string& _name) const { - for(size_t i=0; i<m_descriptions.getDescriptions().size(); ++i) - { - const auto& description = m_descriptions.getDescriptions()[i]; - if(description.name == _name) - return static_cast<ParameterType>(i); - } - - return Param_Invalid; + const auto i = getParameterIndexByName(_name); + if(i == InvalidParameterIndex) + return Param_Invalid; + return static_cast<ParameterType>(i); } void Controller::sendSysEx(const SysEx &msg) const @@ -602,4 +456,15 @@ namespace Virus m_virusOut.insert(m_virusOut.end(), newData.begin(), newData.end()); } + + void Controller::sendParameterChange(const pluginLib::Parameter& _parameter, uint8_t _value) + { + const auto& desc = _parameter.getDescription(); + sendSysEx(constructMessage({static_cast<uint8_t>(desc.page), _parameter.getPart(), desc.index, _value})); + } + + pluginLib::Parameter* Controller::createParameter(pluginLib::Controller& _controller, const pluginLib::Description& _desc, uint8_t _part, int _uid) + { + return new Parameter(_controller, _desc, _part, _uid); + } }; // namespace Virus diff --git a/source/jucePlugin/VirusController.h b/source/jucePlugin/VirusController.h @@ -4,6 +4,7 @@ #include "VirusParameterType.h" #include "../jucePluginLib/parameterdescriptions.h" +#include "../jucePluginLib/controller.h" #include "../virusLib/microcontrollerTypes.h" @@ -13,13 +14,8 @@ class AudioPluginAudioProcessor; namespace Virus { - enum EnvelopeType - { - Env_Amp, - Env_Filter - }; using SysEx = std::vector<uint8_t>; - class Controller : private juce::Timer + class Controller : public pluginLib::Controller, juce::Timer { public: static constexpr size_t kDataSizeInBytes = 256; // same for multi and single @@ -41,8 +37,6 @@ namespace Virus using Singles = std::array<std::array<SinglePatch, 128>, 8>; using Multis = std::array<MultiPatch, 128>; - friend Parameter; - static constexpr auto kNameLength = 10; Controller(AudioPluginAudioProcessor &, unsigned char deviceId = 0x00); @@ -51,18 +45,15 @@ namespace Virus // this is called by the plug-in on audio thread! void dispatchVirusOut(const std::vector<synthLib::SMidiEvent> &); - void printMessage(const SysEx &) const; + void sendParameterChange(const pluginLib::Parameter& _parameter, uint8_t _value) override; - ParameterType getParameterTypeByName(const std::string& _name) const; + pluginLib::Parameter* createParameter(pluginLib::Controller& _controller, const pluginLib::Description& _desc, uint8_t _part, int _uid) override; + + static void printMessage(const SysEx &); - // currently Value as I figure out what's the best approach - // ch - [0-15] - // bank - [0-2] (ABC) - // paramIndex - [0-127] + ParameterType getParameterTypeByName(const std::string& _name) const; juce::Value* getParamValue(uint8_t ch, uint8_t bank, uint8_t paramIndex); - juce::Value* getParamValue(ParameterType _param); - Parameter* getParameter(ParameterType _param) const; - Parameter *getParameter(ParameterType _param, uint8_t _part) const; + virusLib::VirusModel getVirusModel() const; // bank - 0-1 (AB) juce::StringArray getSinglePresetNames(virusLib::BankNumber bank) const; @@ -105,46 +96,16 @@ namespace Virus Singles m_singles; MultiPatch m_currentMulti; - struct ParamIndex - { - uint8_t page; - uint8_t partNum; - uint8_t paramNum; - bool operator<(ParamIndex const &rhs) const - { - if (page < rhs.page) return false; - if (page > rhs.page) return true; - if (partNum < rhs.partNum) return false; - if (partNum > rhs.partNum) return true; - if (paramNum < rhs.paramNum) return false; - if (paramNum > rhs.paramNum) return true; - return false; - } - }; - - using ParameterList = std::vector<Parameter*>; - - std::map<ParamIndex, ParameterList> m_synthInternalParams; - std::map<ParamIndex, ParameterList> m_synthParams; // exposed and managed by audio processor - std::array<ParameterList, 16> m_paramsByParamType; - std::vector<std::unique_ptr<Parameter>> m_synthInternalParamList; - - void registerParams(); - // tries to find synth param in both internal and host. - // @return found parameter or nullptr if none found. - const ParameterList& findSynthParam(uint8_t _part, uint8_t _page, uint8_t _paramIndex); - const ParameterList& findSynthParam(const ParamIndex& _paramIndex); - // unchecked copy for patch data bytes static inline uint8_t copyData(const SysEx &src, int startPos, std::array<uint8_t, kDataSizeInBytes>& dst); - template <typename T> juce::String parseAsciiText(const T &, int startPos) const; + template <typename T> static juce::String parseAsciiText(const T &, int startPos); void parseSingle(const SysEx &); void parseMulti(const SysEx &); void parseParamChange(const SysEx &); void parseControllerDump(synthLib::SMidiEvent &); - AudioPluginAudioProcessor &m_processor; + AudioPluginAudioProcessor& m_processor; juce::CriticalSection m_eventQueueLock; std::vector<synthLib::SMidiEvent> m_virusOut; unsigned char m_deviceId; @@ -152,6 +113,5 @@ namespace Virus uint8_t m_currentProgram[16]{}; uint8_t m_currentPart = 0; juce::PropertiesFile *m_config; - pluginLib::ParameterDescriptions m_descriptions; }; }; // namespace Virus diff --git a/source/jucePlugin/VirusParameter.cpp b/source/jucePlugin/VirusParameter.cpp @@ -1,126 +1,27 @@ #include "VirusParameter.h" -#include "VirusController.h" - namespace Virus { - Parameter::Parameter(Controller &ctrl, const pluginLib::Description& desc, const uint8_t partNum, const int uniqueId) : - RangedAudioParameter(genId(desc, partNum, uniqueId), "Ch " + juce::String(partNum + 1) + " " + desc.name), m_ctrl(ctrl), - m_desc(desc), m_partNum(partNum), m_uniqueId(uniqueId) + Parameter::Parameter(pluginLib::Controller& _controller, const pluginLib::Description& _desc, uint8_t _partNum, int _uniqueId) + : pluginLib::Parameter(_controller, _desc, _partNum, _uniqueId) { - m_range.start = static_cast<float>(m_desc.range.getStart()); - m_range.end = static_cast<float>(m_desc.range.getEnd()); - m_range.interval = m_desc.isDiscrete || m_desc.isBool ? 1.0f : 0.0f; - m_value.addListener(this); - } - - void Parameter::valueChanged(juce::Value &) - { - const uint8_t value = roundToInt(m_value.getValue()); - jassert (m_range.getRange().contains(value) || m_range.end == value); - if (value != m_lastValue) - { - // ignore initial update - if(m_lastValue != -1) - m_ctrl.sendSysEx(m_ctrl.constructMessage({static_cast<uint8_t>(m_desc.page), m_partNum, m_desc.index, value})); - m_lastValue = value; - } - if (onValueChanged) - onValueChanged(); - } - - void Parameter::setLinkedValue(const int _value) - { - const int newValue = juce::roundToInt(m_range.getRange().clipValue(static_cast<float>(_value))); - - if (newValue == m_lastValue) - return; - - m_lastValue = newValue; - - if(getDescription().isPublic) - { - beginChangeGesture(); - setValueNotifyingHost(convertTo0to1(static_cast<float>(newValue))); - endChangeGesture(); - } - else - { - m_value.setValue(newValue); - } - } - - bool Parameter::isMetaParameter() const - { - return !m_linkedParameters.empty(); - } - - void Parameter::setValue(float newValue) - { - if (m_changingLinkedValues) - return; - - m_value.setValue(convertFrom0to1(newValue)); - - m_changingLinkedValues = true; - - for (const auto& parameter : m_linkedParameters) - { - if(!parameter->m_changingLinkedValues) - parameter->setLinkedValue(m_value.getValue()); - } - - m_changingLinkedValues = false; } - void Parameter::setValueFromSynth(int newValue, const bool notifyHost) + float Parameter::getDefaultValue() const { - if (newValue == m_lastValue) - return; - - m_lastValue = newValue; - - if (notifyHost && getDescription().isPublic) - { - beginChangeGesture(); - setValueNotifyingHost(convertTo0to1(static_cast<float>(newValue))); - endChangeGesture(); - } - else - { - m_value.setValue(newValue); - } - - if (m_changingLinkedValues) - return; - - m_changingLinkedValues = true; - - for (const auto& p : m_linkedParameters) - p->setLinkedValue(newValue); - - m_changingLinkedValues = false; - } - - juce::String Parameter::genId(const pluginLib::Description &d, const int part, const int uniqueId) - { - if(uniqueId > 0) - return juce::String::formatted("%d_%d_%d_%d", static_cast<int>(d.page), part, d.index, uniqueId); - return juce::String::formatted("%d_%d_%d", static_cast<int>(d.page), part, d.index); - } - - void Parameter::addLinkedParameter(Parameter* _param) - { - if (_param == this) - return; - - for (auto* p : m_linkedParameters) - { - _param->m_linkedParameters.insert(p); - p->m_linkedParameters.insert(_param); - } - - m_linkedParameters.insert(_param); - _param->m_linkedParameters.insert(this); + const auto& desc = getDescription(); + + // default value should probably be in description instead of hardcoded like this. + if (desc.index == 21 || desc.index == 31) // osc keyfollows + return (64.0f + 32.0f) / 127.0f; + if (desc.index == 36) // osc vol / saturation + return 0.5f; + if (desc.index == 40) // filter1 cutoffs + return 1.0f; + if(desc.index == 41) //filter 2 + return 0.5f; + if(desc.index == 91) // patch volume + return 100.0f / 127.0f; + return desc.isBipolar ? 0.5f : 0.0f; /* maybe return from ROM state? */ } -} // namespace Virus +} diff --git a/source/jucePlugin/VirusParameter.h b/source/jucePlugin/VirusParameter.h @@ -1,83 +1,15 @@ #pragma once -#include <set> - -#include <juce_audio_processors/juce_audio_processors.h> - -#include "../jucePluginLib/parameterdescription.h" +#include "../jucePluginLib/parameter.h" namespace Virus { class Controller; - class Parameter : juce::Value::Listener, public juce::RangedAudioParameter + class Parameter : public pluginLib::Parameter { public: - Parameter(Controller &, const pluginLib::Description& desc, uint8_t partNum, int uniqueId); - - juce::Value &getValueObject() { return m_value; }; - const juce::Value &getValueObject() const { return m_value; }; - - const pluginLib::Description& getDescription() const { return m_desc; }; - - const juce::NormalisableRange<float> &getNormalisableRange() const override { return m_range; } - - bool isMetaParameter() const override; - - float getValue() const override { return convertTo0to1(m_value.getValue()); } - void setValue(float newValue) override; - void setValueFromSynth(int newValue, bool notifyHost = true); - float getDefaultValue() const override { - // default value should probably be in description instead of hardcoded like this. - if (m_desc.index == 21 || m_desc.index == 31) // osc keyfollows - return (64.0f + 32.0f) / 127.0f; - if (m_desc.index == 36) // osc vol / saturation - return 0.5f; - if (m_desc.index == 40) // filter1 cutoffs - return 1.0f; - if(m_desc.index == 41) //filter 2 - return 0.5f; - if(m_desc.index == 91) // patch volume - return 100.0f / 127.0f; - return m_desc.isBipolar ? 0.5f : 0.0f; /* maybe return from ROM state? */ - } - bool isDiscrete() const override { return m_desc.isDiscrete; } - bool isBoolean() const override { return m_desc.isBool; } - bool isBipolar() const { return m_desc.isBipolar; } - - float getValueForText(const juce::String &text) const override - { - const auto res = m_desc.valueList.textToValue(std::string(text.getCharPointer())); - return convertTo0to1(static_cast<float>(res)); - } - - juce::String getText(float normalisedValue, int /*maximumStringLength*/) const override - { - const auto v = convertFrom0to1(normalisedValue); - return m_desc.valueList.valueToText(juce::roundToInt(v)); - } - - // allow 'injecting' additional code for specific parameter. - // eg. multi/single value change requires triggering more logic. - std::function<void()> onValueChanged = {}; - - void addLinkedParameter(Parameter* _param); - - int getUniqueId() const { return m_uniqueId; } - - private: - static juce::String genId(const pluginLib::Description &d, int part, int uniqueId); - void valueChanged(juce::Value &) override; - void setLinkedValue(int _value); - - Controller &m_ctrl; - const pluginLib::Description m_desc; - juce::NormalisableRange<float> m_range; - const uint8_t m_partNum; - const int m_uniqueId; // 0 for all unique parameters, > 0 if multiple Parameter instances reference a single synth parameter - int m_lastValue{-1}; - juce::Value m_value; - std::set<Parameter*> m_linkedParameters; - bool m_changingLinkedValues = false; - }; + Parameter(pluginLib::Controller& _controller, const pluginLib::Description& _desc, uint8_t _partNum, int _uniqueId); + float getDefaultValue() const override; + }; } // namespace Virus diff --git a/source/jucePlugin/VirusParameterBinding.cpp b/source/jucePlugin/VirusParameterBinding.cpp @@ -74,7 +74,7 @@ void VirusParameterBinding::bind(juce::Slider &_slider, Virus::ParameterType _pa } void VirusParameterBinding::bind(juce::Slider &_slider, Virus::ParameterType _param, const uint8_t _part) { - const auto v = m_processor.getController().getParameter(_param, _part == CurrentPart ? m_processor.getController().getCurrentPart() : _part); + const auto v = dynamic_cast<Virus::Parameter*>(m_processor.getController().getParameter(_param, _part == CurrentPart ? m_processor.getController().getCurrentPart() : _part)); if (!v) { @@ -108,7 +108,7 @@ void VirusParameterBinding::bind(juce::ComboBox& _combo, Virus::ParameterType _p void VirusParameterBinding::bind(juce::ComboBox& _combo, const Virus::ParameterType _param, uint8_t _part) { - const auto v = m_processor.getController().getParameter(_param, _part == CurrentPart ? m_processor.getController().getCurrentPart() : _part); + const auto v = dynamic_cast<Virus::Parameter*>(m_processor.getController().getParameter(_param, _part == CurrentPart ? m_processor.getController().getCurrentPart() : _part)); if (!v) { assert(false && "Failed to find parameter"); @@ -152,7 +152,7 @@ void VirusParameterBinding::bind(juce::ComboBox& _combo, const Virus::ParameterT void VirusParameterBinding::bind(juce::Button &_btn, const Virus::ParameterType _param) { - const auto v = m_processor.getController().getParameter(_param, m_processor.getController().getCurrentPart()); + const auto v = dynamic_cast<Virus::Parameter*>(m_processor.getController().getParameter(_param, m_processor.getController().getCurrentPart())); if (!v) { assert(false && "Failed to find parameter"); diff --git a/source/jucePluginLib/CMakeLists.txt b/source/jucePluginLib/CMakeLists.txt @@ -4,6 +4,7 @@ project(jucePluginLib VERSION ${CMAKE_PROJECT_VERSION}) set(SOURCES controller.cpp controller.h midipacket.cpp midipacket.h + parameter.cpp parameter.h parameterdescription.cpp parameterdescription.h parameterdescriptions.cpp parameterdescriptions.h ) diff --git a/source/jucePluginLib/controller.cpp b/source/jucePluginLib/controller.cpp @@ -1,5 +1,159 @@ #include "controller.h" +#include "parameter.h" + namespace pluginLib { + Controller::Controller(const std::string& _parameterDescJson) : m_descriptions(_parameterDescJson) + { + } + + void Controller::registerParams(juce::AudioProcessor& _processor) + { + auto globalParams = std::make_unique<juce::AudioProcessorParameterGroup>("global", "Global", "|"); + + std::map<ParamIndex, int> knownParameterIndices; + + for (uint8_t part = 0; part < 16; part++) + { + m_paramsByParamType[part].reserve(m_descriptions.getDescriptions().size()); + + const auto partNumber = juce::String(part + 1); + auto group = + std::make_unique<juce::AudioProcessorParameterGroup>("ch" + partNumber, "Ch " + partNumber, "|"); + + uint32_t parameterDescIndex = 0; + for (const auto& desc : m_descriptions.getDescriptions()) + { + ++parameterDescIndex; + + const ParamIndex idx = {static_cast<uint8_t>(desc.page), part, desc.index}; + + int uid = 0; + + auto itKnownParamIdx = knownParameterIndices.find(idx); + + if(itKnownParamIdx == knownParameterIndices.end()) + knownParameterIndices.insert(std::make_pair(idx, 0)); + else + uid = ++itKnownParamIdx->second; + + std::unique_ptr<Parameter> p; + p.reset(createParameter(*this, desc, part, uid)); + + if(uid > 0) + { + const auto& existingParams = findSynthParam(idx); + + for (auto& existingParam : existingParams) + existingParam->addLinkedParameter(p.get()); + } + + m_paramsByParamType[part].push_back(p.get()); + + const bool isNonPartExclusive = (desc.classFlags & (int)pluginLib::ParameterClass::Global) || (desc.classFlags & (int)pluginLib::ParameterClass::NonPartSensitive); + if (isNonPartExclusive) + { + if (part != 0) + continue; // only register on first part! + } + if (p->getDescription().isPublic) + { + // lifecycle managed by Juce + + auto itExisting = m_synthParams.find(idx); + if (itExisting != m_synthParams.end()) + { + itExisting->second.push_back(p.get()); + } + else + { + ParameterList params; + params.emplace_back(p.get()); + m_synthParams.insert(std::make_pair(idx, std::move(params))); + } + + if (isNonPartExclusive) + { + jassert(part == 0); + globalParams->addChild(std::move(p)); + } + else + group->addChild(std::move(p)); + } + else + { + // lifecycle handled by us + + auto itExisting = m_synthInternalParams.find(idx); + if (itExisting != m_synthInternalParams.end()) + { + itExisting->second.push_back(p.get()); + } + else + { + ParameterList params; + params.emplace_back(p.get()); + m_synthInternalParams.insert(std::make_pair(idx, std::move(params))); + } + m_synthInternalParamList.emplace_back(std::move(p)); + } + } + _processor.addParameterGroup(std::move(group)); + } + _processor.addParameterGroup(std::move(globalParams)); + } + + const Controller::ParameterList& Controller::findSynthParam(const uint8_t _part, const uint8_t _page, const uint8_t _paramIndex) + { + const ParamIndex paramIndex{ _page, _part, _paramIndex }; + + return findSynthParam(paramIndex); + } + + const Controller::ParameterList& Controller::findSynthParam(const ParamIndex& _paramIndex) + { + const auto it = m_synthParams.find(_paramIndex); + + if (it != m_synthParams.end()) + return it->second; + + const auto iti = m_synthInternalParams.find(_paramIndex); + + if (iti == m_synthInternalParams.end()) + { + static ParameterList empty; + return empty; + } + + return iti->second; + } + + juce::Value* Controller::getParamValueObject(const uint32_t _index) + { + const auto res = getParameter(_index); + return res ? &res->getValueObject() : nullptr; + } + + Parameter* Controller::getParameter(const uint32_t _index) const + { + return getParameter(_index, 0); + } + + Parameter* Controller::getParameter(const uint32_t _index, const uint8_t _part) const + { + if (_part >= m_paramsByParamType.size()) + return nullptr; + + if (_index >= m_paramsByParamType[_part].size()) + return nullptr; + + return m_paramsByParamType[_part][_index]; + } + + uint32_t Controller::getParameterIndexByName(const std::string& _name) const + { + uint32_t index; + return m_descriptions.getIndexByName(index, _name) ? index : InvalidParameterIndex; + } } diff --git a/source/jucePluginLib/controller.h b/source/jucePluginLib/controller.h @@ -1,5 +1,62 @@ #pragma once +#include "parameterdescriptions.h" +#include "parameter.h" + +#include <string> + namespace pluginLib { + class Controller + { + public: + static constexpr uint32_t InvalidParameterIndex = 0xffffffff; + + explicit Controller(const std::string& _parameterDescJson); + + virtual void sendParameterChange(const Parameter& _parameter, uint8_t _value) = 0; + + juce::Value* getParamValueObject(uint32_t _index); + Parameter* getParameter(uint32_t _index) const; + Parameter* getParameter(uint32_t _index, uint8_t _part) const; + + uint32_t getParameterIndexByName(const std::string& _name) const; + + protected: + virtual Parameter* createParameter(Controller& _controller, const Description& _desc, uint8_t _part, int _uid) = 0; + void registerParams(juce::AudioProcessor& _processor); + + private: + + ParameterDescriptions m_descriptions; + + struct ParamIndex + { + uint8_t page; + uint8_t partNum; + uint8_t paramNum; + bool operator<(ParamIndex const &rhs) const + { + if (page < rhs.page) return false; + if (page > rhs.page) return true; + if (partNum < rhs.partNum) return false; + if (partNum > rhs.partNum) return true; + if (paramNum < rhs.paramNum) return false; + if (paramNum > rhs.paramNum) return true; + return false; + } + }; + + using ParameterList = std::vector<Parameter*>; + + // tries to find synth param in both internal and host + protected: + const ParameterList& findSynthParam(uint8_t _part, uint8_t _page, uint8_t _paramIndex); + const ParameterList& findSynthParam(const ParamIndex& _paramIndex); + + std::map<ParamIndex, ParameterList> m_synthInternalParams; + std::map<ParamIndex, ParameterList> m_synthParams; // exposed and managed by audio processor + std::array<ParameterList, 16> m_paramsByParamType; + std::vector<std::unique_ptr<Parameter>> m_synthInternalParamList; + }; } diff --git a/source/jucePluginLib/parameter.cpp b/source/jucePluginLib/parameter.cpp @@ -0,0 +1,126 @@ +#include "parameter.h" + +#include "controller.h" + +namespace pluginLib +{ + Parameter::Parameter(Controller &ctrl, const pluginLib::Description& _desc, const uint8_t _partNum, const int uniqueId) : + juce::RangedAudioParameter(genId(_desc, _partNum, uniqueId), "Ch " + juce::String(_partNum + 1) + " " + _desc.name), m_ctrl(ctrl), + m_desc(_desc), m_partNum(_partNum), m_uniqueId(uniqueId) + { + m_range.start = static_cast<float>(m_desc.range.getStart()); + m_range.end = static_cast<float>(m_desc.range.getEnd()); + m_range.interval = m_desc.isDiscrete || m_desc.isBool ? 1.0f : 0.0f; + m_value.addListener(this); + } + + void Parameter::valueChanged(juce::Value &) + { + const uint8_t value = roundToInt(m_value.getValue()); + jassert (m_range.getRange().contains(value) || m_range.end == value); + if (value != m_lastValue) + { + // ignore initial update + if(m_lastValue != -1) + m_ctrl.sendParameterChange(*this, value); + m_lastValue = value; + } + if (onValueChanged) + onValueChanged(); + } + + void Parameter::setLinkedValue(const int _value) + { + const int newValue = juce::roundToInt(m_range.getRange().clipValue(static_cast<float>(_value))); + + if (newValue == m_lastValue) + return; + + m_lastValue = newValue; + + if(getDescription().isPublic) + { + beginChangeGesture(); + setValueNotifyingHost(convertTo0to1(static_cast<float>(newValue))); + endChangeGesture(); + } + else + { + m_value.setValue(newValue); + } + } + + bool Parameter::isMetaParameter() const + { + return !m_linkedParameters.empty(); + } + + void Parameter::setValue(float newValue) + { + if (m_changingLinkedValues) + return; + + m_value.setValue(convertFrom0to1(newValue)); + + m_changingLinkedValues = true; + + for (const auto& parameter : m_linkedParameters) + { + if(!parameter->m_changingLinkedValues) + parameter->setLinkedValue(m_value.getValue()); + } + + m_changingLinkedValues = false; + } + + void Parameter::setValueFromSynth(int newValue, const bool notifyHost) + { + if (newValue == m_lastValue) + return; + + m_lastValue = newValue; + + if (notifyHost && getDescription().isPublic) + { + beginChangeGesture(); + setValueNotifyingHost(convertTo0to1(static_cast<float>(newValue))); + endChangeGesture(); + } + else + { + m_value.setValue(newValue); + } + + if (m_changingLinkedValues) + return; + + m_changingLinkedValues = true; + + for (const auto& p : m_linkedParameters) + p->setLinkedValue(newValue); + + m_changingLinkedValues = false; + } + + juce::String Parameter::genId(const pluginLib::Description &d, const int part, const int uniqueId) + { + if(uniqueId > 0) + return juce::String::formatted("%d_%d_%d_%d", static_cast<int>(d.page), part, d.index, uniqueId); + return juce::String::formatted("%d_%d_%d", static_cast<int>(d.page), part, d.index); + } + + void Parameter::addLinkedParameter(Parameter* _param) + { + if (_param == this) + return; + + for (auto* p : m_linkedParameters) + { + _param->m_linkedParameters.insert(p); + p->m_linkedParameters.insert(_param); + } + + m_linkedParameters.insert(_param); + _param->m_linkedParameters.insert(this); + } +} diff --git a/source/jucePluginLib/parameter.h b/source/jucePluginLib/parameter.h @@ -0,0 +1,74 @@ +#pragma once + +#include <juce_audio_processors/juce_audio_processors.h> + +#include <set> + +#include "parameterdescription.h" + +namespace pluginLib +{ + struct Description; + + class Controller; + + class Parameter : juce::Value::Listener, public juce::RangedAudioParameter + { + public: + Parameter(Controller& _controller, const Description& _desc, uint8_t _partNum, int uniqueId); + + juce::Value &getValueObject() { return m_value; }; + const juce::Value &getValueObject() const { return m_value; }; + + const Description& getDescription() const { return m_desc; }; + + uint8_t getPart() const { return m_partNum; } + + const juce::NormalisableRange<float> &getNormalisableRange() const override { return m_range; } + + bool isMetaParameter() const override; + + float getValue() const override { return convertTo0to1(m_value.getValue()); } + void setValue(float newValue) override; + void setValueFromSynth(int newValue, bool notifyHost = true); + + bool isDiscrete() const override { return m_desc.isDiscrete; } + bool isBoolean() const override { return m_desc.isBool; } + bool isBipolar() const { return m_desc.isBipolar; } + + float getValueForText(const juce::String &text) const override + { + const auto res = m_desc.valueList.textToValue(std::string(text.getCharPointer())); + return convertTo0to1(static_cast<float>(res)); + } + + juce::String getText(float normalisedValue, int /*maximumStringLength*/) const override + { + const auto v = convertFrom0to1(normalisedValue); + return m_desc.valueList.valueToText(juce::roundToInt(v)); + } + + // allow 'injecting' additional code for specific parameter. + // eg. multi/single value change requires triggering more logic. + std::function<void()> onValueChanged = {}; + + void addLinkedParameter(Parameter* _param); + + int getUniqueId() const { return m_uniqueId; } + + private: + static juce::String genId(const Description &d, int part, int uniqueId); + void valueChanged(juce::Value &) override; + void setLinkedValue(int _value); + + Controller &m_ctrl; + const Description m_desc; + juce::NormalisableRange<float> m_range; + const uint8_t m_partNum; + const int m_uniqueId; // 0 for all unique parameters, > 0 if multiple Parameter instances reference a single synth parameter + int m_lastValue{-1}; + juce::Value m_value; + std::set<Parameter*> m_linkedParameters; + bool m_changingLinkedValues = false; + }; +}