diff --git a/Source/ConfigParserUtil.cpp b/Source/ConfigParserUtil.cpp index 20cb474..fb19351 100644 --- a/Source/ConfigParserUtil.cpp +++ b/Source/ConfigParserUtil.cpp @@ -1,6 +1,6 @@ #include "ConfigParserUtil.h" -int ConfigParserUtil::keyNameToNumber(const juce::String& keyName, const int octaveForMiddleC) +int ConfigParserUtil::keyNameToNumber(const juce::String& keyName) { static const char* const noteNames[] = { "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B", "", "Db", "", "Eb", "", "", "Gb", "", "Ab", "", "Bb" }; diff --git a/Source/ConfigParserUtil.h b/Source/ConfigParserUtil.h index 096e6cc..a381497 100644 --- a/Source/ConfigParserUtil.h +++ b/Source/ConfigParserUtil.h @@ -2,8 +2,10 @@ #include "JuceHeader.h" +const int octaveForMiddleC = 3; + class ConfigParserUtil { public: - // Adapted from https://forum.juce.com/t/midimessage-keynametonumber/9904 - static int keyNameToNumber(const juce::String& keyName, const int octaveForMiddleC); + // Adapted from https://forum.juce.com/t/midimessage-keynametonumber/9904 + static int keyNameToNumber(const juce::String& keyName); }; \ No newline at end of file diff --git a/Source/Configuration.cpp b/Source/Configuration.cpp index 0d188ad..59dcc58 100644 --- a/Source/Configuration.cpp +++ b/Source/Configuration.cpp @@ -2,7 +2,7 @@ #include "InputTreeRootNode.h" #include "ConfigParserUtil.h" -Configuration::Configuration() : Configuration("\n \n 0\n \n \n \n") +Configuration::Configuration() : Configuration("\n \n 0\n \n \n \n") { } @@ -35,8 +35,24 @@ Configuration::Configuration(const std::string& xml) for (const auto& rangeElement : settingsRootElement->getChildWithTagNameIterator("range")) { std::string rangeModeText = rangeElement->getStringAttribute("boundary", "lower").toStdString(); int* targetVariable = rangeModeText == "upper" ? &rangeUpperBoundary : &rangeLowerBoundary; - int noteNumber = ConfigParserUtil::keyNameToNumber(rangeElement->getAllSubText(), 3); + int noteNumber = ConfigParserUtil::keyNameToNumber(rangeElement->getAllSubText()); *targetVariable = noteNumber; + +#ifdef DEBUG + DBG("range" << rangeModeText << " : " << noteNumber); +#endif + } + + // Keyswitches + for (const auto& keyswitchElement : settingsRootElement->getChildWithTagNameIterator("keyswitches")) { + std::string keyswitchModeText = keyswitchElement->getStringAttribute("boundary", "lower").toStdString(); + int* targetVariable = keyswitchModeText == "upper" ? &keyswitchUpperBoundary : &keyswitchLowerBoundary; + int noteNumber = ConfigParserUtil::keyNameToNumber(keyswitchElement->getAllSubText()); + *targetVariable = noteNumber; + +#ifdef DEBUG + DBG("keyswitches" << keyswitchModeText << " : " << noteNumber); +#endif } // Blocklist @@ -46,7 +62,7 @@ Configuration::Configuration(const std::string& xml) blocked.insert(targetText.trim().toStdString()); } else { - blocked.insert(std::to_string(ConfigParserUtil::keyNameToNumber(targetText, 3))); + blocked.insert(std::to_string(ConfigParserUtil::keyNameToNumber(targetText))); } } } @@ -109,6 +125,11 @@ bool Configuration::isInRange(int noteNumber) return noteNumber >= rangeLowerBoundary && noteNumber <= rangeUpperBoundary; } +bool Configuration::isKeyswitch(int noteNumber) +{ + return noteNumber >= keyswitchLowerBoundary && noteNumber <= keyswitchUpperBoundary; +} + bool Configuration::isBlocked(const juce::MidiMessage& message) { if (message.isController()) { diff --git a/Source/Configuration.h b/Source/Configuration.h index 459e2f4..535088b 100644 --- a/Source/Configuration.h +++ b/Source/Configuration.h @@ -16,6 +16,7 @@ class Configuration { void updateSampleRate(double sampleRate); int getLatencySamples(); bool isInRange(int noteNumber); + bool isKeyswitch(int noteNumber); bool isBlocked(const juce::MidiMessage& message); std::unordered_set getTagsForNote(NoteContext& context); @@ -29,6 +30,8 @@ class Configuration { double latency = 0.0; int rangeLowerBoundary = INT_MIN; int rangeUpperBoundary = INT_MAX; + int keyswitchLowerBoundary = INT_MAX; + int keyswitchUpperBoundary = INT_MIN; std::unique_ptr inputTreeRoot; std::map> outputList; diff --git a/Source/InputTreeCase.cpp b/Source/InputTreeCase.cpp index 6f7978b..5993c14 100644 --- a/Source/InputTreeCase.cpp +++ b/Source/InputTreeCase.cpp @@ -1,12 +1,19 @@ #include "InputTreeCase.h" -InputTreeCase::InputTreeCase(const juce::XmlElement& source) +InputTreeCase::InputTreeCase(const juce::XmlElement& source, int keyswitch) { - equals = source.getIntAttribute("equals", -1); - greater = source.getIntAttribute("greater", INT_MIN); - less = source.getIntAttribute("less", INT_MAX); + // keyswitch targets have no 'case' statement, so the 'equals' value + // needs to be specified here manually + if (keyswitch != -1) { + equals = keyswitch; + } + else { + equals = source.getIntAttribute("equals", -1); + greater = source.getIntAttribute("greater", INT_MIN); + less = source.getIntAttribute("less", INT_MAX); - alwaysTrue = source.isTextElement(); + alwaysTrue = source.isTextElement(); + } } bool InputTreeCase::check(int value) diff --git a/Source/InputTreeCase.h b/Source/InputTreeCase.h index 3fe4f91..a88f95c 100644 --- a/Source/InputTreeCase.h +++ b/Source/InputTreeCase.h @@ -4,9 +4,11 @@ class InputTreeCase { public: - InputTreeCase(const juce::XmlElement& source); + InputTreeCase(const juce::XmlElement& source, int keyswitch); bool check(int value); private: - bool alwaysTrue; - int equals, greater, less; + bool alwaysTrue = false; + int equals = -1; + int greater = INT_MIN; + int less = INT_MAX; }; \ No newline at end of file diff --git a/Source/InputTreeSwitchNode.cpp b/Source/InputTreeSwitchNode.cpp index 187bd01..150e623 100644 --- a/Source/InputTreeSwitchNode.cpp +++ b/Source/InputTreeSwitchNode.cpp @@ -19,10 +19,20 @@ InputTreeSwitchNode::InputTreeSwitchNode(const juce::XmlElement& source) targetNumber = std::stoi(trimmed); target = CC; + } + else if (targetStr.starts_with("KS_")) { + std::string trimmed = targetStr.substr(3, targetStr.length()); + try { + targetNumber = ConfigParserUtil::keyNameToNumber(trimmed); + target = KEYSWITCH; + } + catch (std::exception& e) { + throw std::exception("Encountered a node target attribute with an invalid value."); + } } else { try { - targetNumber = ConfigParserUtil::keyNameToNumber(targetStr, 3); + targetNumber = ConfigParserUtil::keyNameToNumber(targetStr); target = NOTE; } catch (std::exception& e) { @@ -31,10 +41,15 @@ InputTreeSwitchNode::InputTreeSwitchNode(const juce::XmlElement& source) } } + int keyswitch = -1; + if (target == KEYSWITCH) { + keyswitch = targetNumber; + } + for (const auto& caseEntryElement : source.getChildIterator()) { if (caseEntryElement->getTagName() == "case") { int insertIndex = children.size(); - children.emplace_back(std::make_tuple(InputTreeCase(*caseEntryElement), std::vector>())); + children.emplace_back(std::make_tuple(InputTreeCase(*caseEntryElement, keyswitch), std::vector>())); for (const auto& caseChildElement : caseEntryElement->getChildIterator()) { IInputTreeNode* child = InputTreeNodeFactory::make(*caseChildElement); @@ -43,7 +58,7 @@ InputTreeSwitchNode::InputTreeSwitchNode(const juce::XmlElement& source) } else { int insertIndex = children.size(); - children.emplace_back(std::make_tuple(InputTreeCase(*caseEntryElement), std::vector>())); + children.emplace_back(std::make_tuple(InputTreeCase(*caseEntryElement, keyswitch), std::vector>())); IInputTreeNode* child = InputTreeNodeFactory::make(*caseEntryElement); std::get<1>(children[insertIndex]).emplace_back(child); @@ -85,8 +100,13 @@ int InputTreeSwitchNode::getTargetValue(NoteContext& context) case NOTE: return context.getHeldNoteVelocity(targetNumber); break; + case KEYSWITCH: + return context.getLastKeyswitch(); + break; case PROGRAM: return context.getActiveProgram(); break; + default: + throw std::exception("Invalid target value"); } } diff --git a/Source/InputTreeSwitchNode.h b/Source/InputTreeSwitchNode.h index c6d00cf..8cbfc91 100644 --- a/Source/InputTreeSwitchNode.h +++ b/Source/InputTreeSwitchNode.h @@ -9,7 +9,7 @@ class InputTreeSwitchNode : public IInputTreeNode { InputTreeSwitchNode(const juce::XmlElement& source); NoteContext& visit(NoteContext& context) override; private: - enum TargetType { CC, VELOCITY, LEGATO, LENGTH, NOTE, PROGRAM }; + enum TargetType { CC, VELOCITY, LEGATO, LENGTH, NOTE, KEYSWITCH, PROGRAM }; std::vector>>> children; int targetNumber; diff --git a/Source/NoteContext.cpp b/Source/NoteContext.cpp index 65712c9..5f98a09 100644 --- a/Source/NoteContext.cpp +++ b/Source/NoteContext.cpp @@ -1,12 +1,13 @@ #include "NoteContext.h" -NoteContext::NoteContext(BufferedNote* note, const std::optional& previousNote, int ccStates[128], int heldNotes[128], int program) +NoteContext::NoteContext(BufferedNote* note, const std::optional& previousNote, int ccStates[128], int heldNotes[128], int program, int lastKeyswitch) { this->note = note; this->previousNote = previousNote; this->ccStates = ccStates; this->heldNotes = heldNotes; this->program = program; + this->lastKeyswitch = lastKeyswitch; } int NoteContext::getVelocity() const @@ -24,6 +25,11 @@ int NoteContext::getActiveProgram() const return program; } +int NoteContext::getLastKeyswitch() const +{ + return lastKeyswitch; +} + int NoteContext::getHeldNoteVelocity(const int number) const { return heldNotes[number]; @@ -31,6 +37,11 @@ int NoteContext::getHeldNoteVelocity(const int number) const bool NoteContext::isLegato() const { + // if the previous or current note was or is a keyswitch, this isn't a legato transition + if (note->pitch == lastKeyswitch || (previousNote.has_value() && previousNote->pitch == lastKeyswitch)) { + return false; + } + // 64 samples should definitely be enough to catch a legato even on inaccurate DAWs without creating unintentional legato transitions return previousNote.has_value() && previousNote->allowLegato && llabs(note->startTime - previousNote->endTime.value()) <= 64; } diff --git a/Source/NoteContext.h b/Source/NoteContext.h index 0da0669..eec0e93 100644 --- a/Source/NoteContext.h +++ b/Source/NoteContext.h @@ -5,11 +5,12 @@ class NoteContext { public: - NoteContext(BufferedNote* note, const std::optional& previousNote, int ccStates[128], int heldNotes[128], int program); + NoteContext(BufferedNote* note, const std::optional& previousNote, int ccStates[128], int heldNotes[128], int program, int lastKeyswitch); int getVelocity() const; int getCCValue(const int number) const; int getActiveProgram() const; + int getLastKeyswitch() const; int getHeldNoteVelocity(const int number) const; bool isLegato() const; std::optional getLength() const; @@ -25,4 +26,5 @@ class NoteContext { int* ccStates; int* heldNotes; int program; + int lastKeyswitch; }; \ No newline at end of file diff --git a/Source/OutputListNode.cpp b/Source/OutputListNode.cpp index a9ea28e..6862b04 100644 --- a/Source/OutputListNode.cpp +++ b/Source/OutputListNode.cpp @@ -30,7 +30,7 @@ OutputListNode::OutputListNode(const juce::XmlElement& source) } if (target == NOTE) { - value = ConfigParserUtil::keyNameToNumber(juce::String(valueStr), 3); + value = ConfigParserUtil::keyNameToNumber(juce::String(valueStr)); } else if (target != LEGATO) { if (valueStr.starts_with("CC")) { diff --git a/Source/Versioning.h b/Source/Versioning.h index 75471b7..ddd0d2e 100644 --- a/Source/Versioning.h +++ b/Source/Versioning.h @@ -1,4 +1,4 @@ #pragma once -#define CURRENT_STATE_VERSION 2 -#define CURRENT_CONFIG_VERSION 3 \ No newline at end of file +#define CURRENT_STATE_VERSION 3 +#define CURRENT_CONFIG_VERSION 4 \ No newline at end of file diff --git a/Source/VoiceManager.cpp b/Source/VoiceManager.cpp index ea08823..4eb616a 100644 --- a/Source/VoiceManager.cpp +++ b/Source/VoiceManager.cpp @@ -33,7 +33,7 @@ juce::MidiBuffer VoiceManager::processBuffer(const juce::MidiBuffer& buffer) // ...copy over everything that's not a note or that's outside the range for (auto& message : entry.second) { - if (!message.isNoteOnOrOff() || (message.isNoteOnOrOff() && !configuration->isInRange(message.getNoteNumber()))) { + if (!message.isNoteOnOrOff() || (message.isNoteOnOrOff() && !configuration->isInRange(message.getNoteNumber()) && !configuration->isKeyswitch(message.getNoteNumber()))) { message.setChannel(1); processedBuffer.addEvent(message, time); } @@ -42,7 +42,7 @@ juce::MidiBuffer VoiceManager::processBuffer(const juce::MidiBuffer& buffer) // ...look for a note-off first for (auto& message : entry.second) { - if (message.isNoteOff() && configuration->isInRange(message.getNoteNumber()) && heldNote.has_value() && heldNote == message.getNoteNumber()) { + if (message.isNoteOff() && (configuration->isInRange(message.getNoteNumber()) || configuration->isKeyswitch(message.getNoteNumber())) && heldNote.has_value() && heldNote == message.getNoteNumber()) { heldNote.reset(); message.setChannel(1); @@ -55,8 +55,7 @@ juce::MidiBuffer VoiceManager::processBuffer(const juce::MidiBuffer& buffer) // ...handle note-on for (auto& message : entry.second) { - if (message.isNoteOn() && configuration->isInRange(message.getNoteNumber())) - { + if (message.isNoteOn() && (configuration->isInRange(message.getNoteNumber()) || configuration->isKeyswitch(message.getNoteNumber()))) { // If there's already a playing note, stop it if (heldNote.has_value()) { @@ -65,7 +64,6 @@ juce::MidiBuffer VoiceManager::processBuffer(const juce::MidiBuffer& buffer) } // Play the new note - heldNote = message.getNoteNumber(); message.setChannel(1); diff --git a/Source/VoiceProcessor.cpp b/Source/VoiceProcessor.cpp index 24d5e84..855a254 100644 --- a/Source/VoiceProcessor.cpp +++ b/Source/VoiceProcessor.cpp @@ -133,7 +133,7 @@ std::vector VoiceProcessor::processSample(const std::optional for (const auto& note : bufferedNotes) { if (note->startTime == getReadPosition()) { // Process note that's about to play - everything but start delay is processed here - NoteContext context = NoteContext(note, previousNoteAtReadPosition, readPositionCCStates, readPositionHeldNotes, readPositionProgram); + NoteContext context = NoteContext(note, previousNoteAtReadPosition, readPositionCCStates, readPositionHeldNotes, readPositionProgram, lastKeyswitch); std::unordered_set tags = configuration->getTagsForNote(context); NoteProcessor noteProcessor = NoteProcessor(context, configuration, tags, channel); std::vector results = noteProcessor.getResults(); @@ -165,6 +165,15 @@ std::vector VoiceProcessor::processSample(const std::optional if (message.isNoteOn()) writePositionHeldNotes[message.getNoteNumber()] = message.getVelocity(); if (message.isNoteOff()) writePositionHeldNotes[message.getNoteNumber()] = -1; if (message.isProgramChange()) writePositionProgram = message.getProgramChangeNumber(); + if (configuration->isKeyswitch(message.getNoteNumber())) { + lastKeyswitch = message.getNoteNumber(); + +#ifdef DEBUG + if (message.isNoteOn()) { + DBG("Keyswitch: " << lastKeyswitch); + } +#endif + } } else if (message.isNoteOff() && heldNoteAtWritePosition) { // Note off heldNoteAtWritePosition->endTime = getWritePosition(); @@ -178,7 +187,7 @@ std::vector VoiceProcessor::processSample(const std::optional heldNoteAtWritePosition = newNote; // Process new note - only start delay is processed here - NoteContext context = NoteContext(newNote, previousNoteAtWritePosition, writePositionCCStates, writePositionHeldNotes, writePositionProgram); + NoteContext context = NoteContext(newNote, previousNoteAtWritePosition, writePositionCCStates, writePositionHeldNotes, writePositionProgram, lastKeyswitch); std::unordered_set tags = configuration->getTagsForNote(context); NoteProcessor noteProcessor = NoteProcessor(context, configuration, tags, channel); noteProcessor.applyStartDelay(); diff --git a/Source/VoiceProcessor.h b/Source/VoiceProcessor.h index 60b9792..006d3ff 100644 --- a/Source/VoiceProcessor.h +++ b/Source/VoiceProcessor.h @@ -19,6 +19,7 @@ class VoiceProcessor { int bufferSizeSamples; unsigned long long readHeadPosition = 0; + int lastKeyswitch = -1; std::vector bufferedNotes; std::optional lastWrittenNote; BufferedNote* heldNoteAtWritePosition = nullptr;