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;