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 46eccdd8ba72e277d6bb450dd2678bc5b63ab3b7
parent 6f9fbdca74107e50c0bcb5356b505e832aac0946
Author: dsp56300 <dsp56300@users.noreply.github.com>
Date:   Sun, 24 Nov 2024 13:11:24 +0100

use channel translation to prevent performance channel collision to be able to use CC instead of full patch-sysex to edit parameters, fixes slow parameter changes in case of >1 slot using the same midi channel

Diffstat:
Msource/nord/n2x/n2xJucePlugin/n2xController.cpp | 31++++++-------------------------
Msource/nord/n2x/n2xLib/n2xdevice.cpp | 2+-
Msource/nord/n2x/n2xLib/n2xmiditypes.h | 1+
Msource/nord/n2x/n2xLib/n2xstate.cpp | 146+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Msource/nord/n2x/n2xLib/n2xstate.h | 24+++++++++++++++++++++++-
5 files changed, 136 insertions(+), 68 deletions(-)

diff --git a/source/nord/n2x/n2xJucePlugin/n2xController.cpp b/source/nord/n2x/n2xJucePlugin/n2xController.cpp @@ -9,6 +9,7 @@ #include "dsp56kEmu/logging.h" #include "n2xLib/n2xmiditypes.h" +#include "synthLib/midiTranslator.h" namespace { @@ -31,7 +32,7 @@ namespace namespace n2xJucePlugin { - Controller::Controller(AudioPluginAudioProcessor& _p) : pluginLib::Controller(_p, "parameterDescriptions_n2x.json"), m_state(nullptr) + Controller::Controller(AudioPluginAudioProcessor& _p) : pluginLib::Controller(_p, "parameterDescriptions_n2x.json"), m_state(nullptr, nullptr) { registerParams(_p, [](const uint8_t _part, const bool _isNonPartExclusive) { @@ -230,31 +231,11 @@ namespace n2xJucePlugin const auto parts = m_state.getPartsForMidiChannel(ch); - const auto ev = synthLib::SMidiEvent{synthLib::MidiEventSource::Editor, static_cast<uint8_t>(synthLib::M_CONTROLCHANGE + ch), cc, static_cast<uint8_t>(_value)}; + auto ev = synthLib::SMidiEvent{synthLib::MidiEventSource::Editor, static_cast<uint8_t>(synthLib::M_CONTROLCHANGE + part), cc, static_cast<uint8_t>(_value)}; - if(parts.size() > 1) - { - // this is problematic. We want to edit one part only but two parts receive on the same channel. We have to send a full dump - nonConstParam.setRateLimitMilliseconds(sysexRateLimitMs); - - const auto& name = _parameter.getDescription().name; - - if(name == "Sync" || name == "RingMod" || name == "Distortion") - { - const auto value = combineSyncRingModDistortion(part, 0, false); - setSingleParameter(part, n2x::Sync, value); - } - else - { - setSingleParameter(part, singleParam, static_cast<uint8_t>(_value)); - } - } - else - { - nonConstParam.setRateLimitMilliseconds(0); - m_state.receive(ev); - sendMidiEvent(ev); - } + nonConstParam.setRateLimitMilliseconds(0); + m_state.changeSingleParameter(part, ev); + sendMidiEvent(n2x::State::createPartCC(part, ev)); } void Controller::setSingleParameter(uint8_t _part, n2x::SingleParam _sp, uint8_t _value) diff --git a/source/nord/n2x/n2xLib/n2xdevice.cpp b/source/nord/n2x/n2xLib/n2xdevice.cpp @@ -5,7 +5,7 @@ namespace n2x { - Device::Device() : m_state(&m_hardware) + Device::Device() : m_state(&m_hardware, &getMidiTranslator()) { } diff --git a/source/nord/n2x/n2xLib/n2xmiditypes.h b/source/nord/n2x/n2xLib/n2xmiditypes.h @@ -16,6 +16,7 @@ namespace n2x EmuSetPotPosition = 90, // total dump is: f0, IdClavia, IdDevice, IdN2x, EmuSetPotPosition, KnobType / nibble high, nibble low / f7 EmuGetPotsPosition = 91, + EmuSetPartCC = 92, }; enum SysexIndex diff --git a/source/nord/n2x/n2xLib/n2xstate.cpp b/source/nord/n2x/n2xLib/n2xstate.cpp @@ -4,6 +4,7 @@ #include "n2xhardware.h" #include "synthLib/midiToSysex.h" +#include "synthLib/midiTranslator.h" #include "synthLib/midiTypes.h" namespace n2x @@ -180,7 +181,7 @@ namespace n2x static const MultiDefaultData g_multiDefault = createMultiDefaultData(); - State::State(Hardware* _hardware) : m_hardware(_hardware) + State::State(Hardware* _hardware, synthLib::MidiTranslator* _midiTranslator) : m_hardware(_hardware), m_midiTranslator(_midiTranslator) { for(uint8_t i=0; i<static_cast<uint8_t>(m_singles.size()); ++i) createDefaultSingle(m_singles[i], i); @@ -252,7 +253,41 @@ namespace n2x return false; m_multi.fill(0); std::copy(sysex.begin(), sysex.end(), m_multi.begin()); - send(_ev); + + if (m_midiTranslator) + { + // As we need support for individual midi channels for the editor to adjus each part separately but + // knobs can only be modified via midi CC, we tell the device that parts 0-3 are on midi channels 0-3 even if + // they are not. The midi translator will translate regular midi messages and the editor uses messages that are + // not translated + + m_midiTranslator->clear(); + + MultiDump dump = m_multi; + for (uint8_t i = 0; i < 4; ++i) + { + const auto ch = getPartMidiChannel(dump, i); + setPartMidiChannel(dump, i, i); + m_midiTranslator->addTargetChannel(ch, i); + } + + synthLib::SMidiEvent e; + e.sysex.assign(dump.begin(), dump.end()); + send(e); + } + else + { + send(_ev); + } + + return true; + } + + if (bank == SysexByte::MultiRequestBankEditBuffer) + { + _responses.emplace_back(synthLib::MidiEventSource::Internal); + _responses.back().sysex.assign(m_multi.begin(), m_multi.end()); + _responses.back().sysex = validateDump(_responses.back().sysex); return true; } @@ -278,6 +313,17 @@ namespace n2x } return true; } + else if (bank == SysexByte::EmuSetPartCC) + { + synthLib::SMidiEvent e; + auto part = sysex[5]; + e.a = sysex[6]; + e.b = sysex[7]; + e.c = sysex[8]; + e.source = _ev.source; + e.offset = _ev.offset; + changeSingleParameter(part, e); + } return false; } @@ -299,54 +345,59 @@ namespace n2x const auto parts = getPartsForMidiChannel(_ev); if(parts.empty()) return false; - - const auto cc = static_cast<ControlChange>(_ev.b); - const auto it = g_controllerMap.find(cc); - if(it == g_controllerMap.end()) - return false; - const SingleParam param = it->second; - const auto offset = getOffsetInSingleDump(param); - switch (param) + for (const auto part : parts) { - case SingleParam::Sync: - // this can either be sync or distortion, they end up in the same midi byte - switch(cc) - { - case ControlChange::CCSync: - for (const auto part : parts) - { - auto v = unpackNibbles(m_singles[part], offset); - v &= ~0x3; - v |= _ev.c & 0x3; - packNibbles(m_singles[part], offset, v); - } - break; - case ControlChange::CCDistortion: - for (const auto part : parts) - { - auto v = unpackNibbles(m_singles[part], offset); - v &= ~(1<<4); - v |= _ev.c << 4; - packNibbles(m_singles[part], offset, v); - } - break; - default: - assert(false && "unexpected control change type"); + if (!changeSingleParameter(part, _ev)) return false; - } - break; - default: - for (const auto part : parts) - packNibbles(m_singles[part], offset, _ev.c); - return true; } + return true; } - return false; default: return false; } } + bool State::changeSingleParameter(const uint8_t _part, const synthLib::SMidiEvent& _ev) + { + const auto cc = static_cast<ControlChange>(_ev.b); + const auto it = g_controllerMap.find(cc); + if(it == g_controllerMap.end()) + return false; + const SingleParam param = it->second; + const auto offset = getOffsetInSingleDump(param); + switch (param) + { + case SingleParam::Sync: + // this can either be sync or distortion, they end up in the same midi byte + switch(cc) + { + case ControlChange::CCSync: + { + auto v = unpackNibbles(m_singles[_part], offset); + v &= ~0x3; + v |= _ev.c & 0x3; + packNibbles(m_singles[_part], offset, v); + } + return true; + case ControlChange::CCDistortion: + { + auto v = unpackNibbles(m_singles[_part], offset); + v &= ~(1<<4); + v |= _ev.c << 4; + packNibbles(m_singles[_part], offset, v); + } + return true; + default: + assert(false && "unexpected control change type"); + return false; + } + break; + default: + packNibbles(m_singles[_part], offset, _ev.c); + return true; + } + } + bool State::changeSingleParameter(const uint8_t _part, const SingleParam _parameter, const uint8_t _value) { if(_part >= m_singles.size()) @@ -546,6 +597,19 @@ namespace n2x return _dump; } + synthLib::SMidiEvent& State::createPartCC(uint8_t _part, synthLib::SMidiEvent& _ccEvent) + { + _ccEvent.sysex = { 0xf0, IdClavia, SysexByte::DefaultDeviceId, IdN2X, + SysexByte::EmuSetPartCC, + _part, + _ccEvent.a, + _ccEvent.b, + _ccEvent.c, + 0xf7 + }; + return _ccEvent; + } + void State::send(const synthLib::SMidiEvent& _e) const { if(_e.source == synthLib::MidiEventSource::Plugin) diff --git a/source/nord/n2x/n2xLib/n2xstate.h b/source/nord/n2x/n2xLib/n2xstate.h @@ -11,6 +11,11 @@ #include "synthLib/midiTypes.h" +namespace synthLib +{ + class MidiTranslator; +} + namespace n2x { class Hardware; @@ -21,7 +26,7 @@ namespace n2x using SingleDump = std::array<uint8_t, g_singleDumpWithNameSize>; using MultiDump = std::array<uint8_t, g_multiDumpWithNameSize>; - explicit State(Hardware* _hardware); + explicit State(Hardware* _hardware, synthLib::MidiTranslator* _midiTranslator); bool getState(std::vector<uint8_t>& _state); bool setState(const std::vector<uint8_t>& _state); @@ -36,6 +41,8 @@ namespace n2x bool receiveNonSysex(const synthLib::SMidiEvent& _ev); + bool changeSingleParameter(uint8_t _part, const synthLib::SMidiEvent& _ev); + bool changeSingleParameter(uint8_t _part, SingleParam _parameter, uint8_t _value); bool changeMultiParameter(MultiParam _parameter, uint8_t _value); @@ -86,6 +93,11 @@ namespace n2x return getMultiParam(_dump, static_cast<MultiParam>(SlotAMidiChannel + _part), 0); } + template<typename TDump> static void setPartMidiChannel(TDump& _dump, const uint8_t _part, const uint8_t _channel) + { + setMultiParam(_dump, static_cast<MultiParam>(SlotAMidiChannel + _part), 0, _channel); + } + uint8_t getMultiParam(const MultiParam _param, const uint8_t _part) const { return getMultiParam(m_multi, _param, _part); @@ -98,6 +110,13 @@ namespace n2x return unpackNibbles<TDump>(_dump, off); } + template<typename TDump> static void setMultiParam(TDump& _dump, const MultiParam _param, const uint8_t _part, const uint8_t _value) + { + const auto off = getOffsetInMultiDump(_param) + (_part << 2); + + packNibbles(_dump, off, _value); + } + template<typename TDump> static uint8_t getSingleParam(const TDump& _dump, const SingleParam _param, const uint8_t _part) { const auto off = getOffsetInSingleDump(_param) + (_part << 2); @@ -130,6 +149,8 @@ namespace n2x static bool isValidPatchName(const std::vector<uint8_t>& _dump); static std::vector<uint8_t> validateDump(const std::vector<uint8_t>& _dump); + static synthLib::SMidiEvent& createPartCC(uint8_t _part, synthLib::SMidiEvent& _ccEvent); + private: template<size_t Size> bool receive(const std::array<uint8_t, Size>& _data) { @@ -142,6 +163,7 @@ namespace n2x void send(const synthLib::SMidiEvent& _e) const; Hardware* m_hardware; + synthLib::MidiTranslator* m_midiTranslator; std::array<SingleDump, 4> m_singles; MultiDump m_multi; std::unordered_map<KnobType, uint8_t> m_knobStates;