diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..93bbd1b
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,154 @@
+name: Foresight
+
+on:
+ push:
+ branches:
+ - "*"
+ tags:
+ - "v*"
+ workflow_dispatch:
+
+# When pushing new commits, cancel any running builds on that branch
+concurrency:
+ group: ${{ github.ref }}
+ cancel-in-progress: true
+
+env:
+ PROJECT_NAME: Foresight
+ BUILD_TYPE: Release
+ BUILD_DIR: Builds
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ DISPLAY: :0 # linux pluginval needs this
+ CMAKE_BUILD_PARALLEL_LEVEL: 3 # Use up to 3 cpus to build juceaide, etc
+ HOMEBREW_NO_INSTALL_CLEANUP: 1
+
+# jobs are run in parallel on different machines
+# all steps run in series
+jobs:
+ build_and_test:
+ name: ${{ matrix.name }}
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false # show all errors for each platform (vs. cancel jobs on error)
+ matrix:
+ include:
+ - name: Windows
+ os: windows-latest
+ pluginval-binary: ./pluginval.exe
+ ccache: sccache
+ - name: macOS
+ os: macos-12
+ pluginval-binary: pluginval.app/Contents/MacOS/pluginval
+ ccache: ccache
+ - name: Linux
+ os: ubuntu-latest
+ pluginval-binary: ./pluginval
+ ccache: ccache
+
+ steps:
+ # This is just easier than debugging different compilers on different platforms
+ - name: Set up Clang
+ if: runner.os != 'macOS'
+ uses: egor-tensin/setup-clang@v1
+
+ # This lets us use sscache on Windows
+ # We need to install ccache here for Windows to grab the right version
+ - name: Install Ninja (Windows)
+ if: runner.os == 'Windows'
+ shell: bash
+ run: choco install ninja ccache
+
+ - name: Install macOS Deps
+ if: ${{ matrix.name == 'macOS' }}
+ run: brew install ninja osxutils
+
+ # This also starts up our "fake" display Xvfb, needed for pluginval
+ - name: Install JUCE's Linux Deps
+ if: runner.os == 'Linux'
+ # Thanks to McMartin & co https://forum.juce.com/t/list-of-juce-dependencies-under-linux/15121/44
+ run: |
+ sudo apt-get update && sudo apt install libasound2-dev libx11-dev libxinerama-dev libxext-dev libfreetype6-dev libwebkit2gtk-4.0-dev libglu1-mesa-dev xvfb ninja-build
+ sudo /usr/bin/Xvfb $DISPLAY &
+
+ - name: Checkout code
+ uses: actions/checkout@v3
+ with:
+ submodules: true # Get JUCE populated
+
+ - name: Setup Environment Variables
+ shell: bash
+ run: |
+ VERSION=$(cat VERSION)
+ ARTIFACTS_PATH="${{ env.BUILD_DIR }}/${{ env.PROJECT_NAME }}_artefacts/${{ env.BUILD_TYPE }}"
+ echo "ARTIFACTS_PATH=${ARTIFACTS_PATH}" >> $GITHUB_ENV
+ echo "VST3_PATH=${ARTIFACTS_PATH}/VST3/${{ env.PROJECT_NAME }}.vst3" >> $GITHUB_ENV
+ echo "AU_PATH=${ARTIFACTS_PATH}/AU/${{ env.PROJECT_NAME }}.component" >> $GITHUB_ENV
+ echo "AUV3_PATH=${ARTIFACTS_PATH}/AUv3/${{ env.PROJECT_NAME }}.appex" >> $GITHUB_ENV
+ echo "PRODUCT_NAME=${{ env.PROJECT_NAME }}-$VERSION-${{ matrix.name }}" >> $GITHUB_ENV
+
+ - name: ccache
+ uses: hendrikmuhs/ccache-action@main
+ with:
+ key: v3-${{ matrix.os }}-${{ matrix.type }}
+ variant: ${{ matrix.ccache }}
+
+ - name: Configure
+ shell: bash
+ run: cmake -B ${{ env.BUILD_DIR }} -G Ninja -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE}} -DCMAKE_C_COMPILER_LAUNCHER=${{ matrix.ccache }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ matrix.ccache }} -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" .
+
+ - name: Build
+ shell: bash
+ run: cmake --build ${{ env.BUILD_DIR }} --config ${{ env.BUILD_TYPE }} --parallel 4
+
+ - name: Pluginval
+ shell: bash
+ run: |
+ curl -LO "https://github.com/Tracktion/pluginval/releases/download/v1.0.3/pluginval_${{ matrix.name }}.zip"
+ 7z x pluginval_${{ matrix.name }}.zip
+ ${{ matrix.pluginval-binary }} --strictness-level 10 --verbose --validate "${{ env.VST3_PATH }}"
+
+ - name: Zip VST3, AU and AUv3 (MacOS)
+ if: runner.os == 'macOS'
+ run: |
+ cmake -E tar cvf ${{ env.PRODUCT_NAME }}.zip --format=zip "${{ env.ARTIFACTS_PATH }}"
+
+ - name: Upload VST3 (Windows)
+ if: runner.os == 'Windows'
+ uses: actions/upload-artifact@v3
+ with:
+ name: ${{ env.PRODUCT_NAME }}.vst3
+ path: "${{ env.VST3_PATH }}/**/*.vst3"
+
+ - name: Upload zipped VST3, AU and AUv3 (MacOS)
+ if: runner.os == 'macOS'
+ uses: actions/upload-artifact@v3
+ with:
+ name: ${{ env.PRODUCT_NAME }}.zip
+ path: ${{ env.PRODUCT_NAME }}.zip
+
+ - name: Upload SO (Linux)
+ if: runner.os == 'Linux'
+ uses: actions/upload-artifact@v3
+ with:
+ name: ${{ env.PRODUCT_NAME }}.so
+ path: "${{ env.VST3_PATH }}/**/*.so"
+
+ release:
+ if: contains(github.ref, 'tags/v')
+ runs-on: ubuntu-latest
+ needs: build_and_test
+
+ steps:
+ - name: Get Artifacts
+ uses: actions/download-artifact@v3
+
+ - name: Create Release
+ uses: softprops/action-gh-release@v1
+ with:
+ prerelease: true
+ # download-artifact puts these files in their own dirs...
+ # Using globs sidesteps having to pass the version around
+ files: |
+ */*.vst3
+ */*.zip
+ */*.so
diff --git a/.gitmodules b/.gitmodules
index 7a8abf5..1142516 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,3 @@
-[submodule "Deps/JUCE"]
- path = Deps/JUCE
+[submodule "deps/juce"]
+ path = deps/juce
url = https://github.com/juce-framework/JUCE
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0a03d22..d7a0ea7 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,10 +1,21 @@
-cmake_minimum_required(VERSION 3.25)
+cmake_minimum_required(VERSION 3.22)
-project(FORESIGHT VERSION 1.2.0)
+# read VERSION file for version
+file(STRINGS VERSION CURRENT_VERSION)
-add_subdirectory(Deps)
+set(CMAKE_OSX_DEPLOYMENT_TARGET "11" CACHE STRING "Support macOS down to Big Sur")
-juce_add_plugin(FORESIGHT
+project(Foresight VERSION ${CURRENT_VERSION})
+
+# to enable use of clang-tidy
+set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
+
+# By default we don't want Xcode schemes to be made for modules, etc
+set(CMAKE_XCODE_GENERATE_SCHEME OFF)
+
+add_subdirectory(deps)
+
+juce_add_plugin(Foresight
COMPANY_NAME ToadsworthLP
COMPANY_WEBSITE "toadsworthlp.github.io"
NEEDS_MIDI_INPUT TRUE
@@ -13,15 +24,15 @@ juce_add_plugin(FORESIGHT
VST3_CATEGORIES Delay
PLUGIN_MANUFACTURER_CODE Twlp
PLUGIN_CODE Fsgt
- FORMATS VST3
+ FORMATS VST3 AU AUv3
PRODUCT_NAME "Foresight"
COPY_PLUGIN_AFTER_BUILD TRUE
VST3_COPY_DIR ${CMAKE_CURRENT_LIST_DIR}/output
)
-juce_generate_juce_header(FORESIGHT)
+juce_generate_juce_header(Foresight)
-target_sources(FORESIGHT
+target_sources(Foresight
PRIVATE
Source/BufferedMidiMessage.cpp
Source/BufferedNote.cpp
@@ -43,9 +54,11 @@ target_sources(FORESIGHT
Source/VoiceProcessor.cpp
)
-target_compile_features(FORESIGHT PRIVATE cxx_std_20)
+target_compile_features(Foresight PRIVATE cxx_std_20)
-target_compile_definitions(FORESIGHT
+target_compile_definitions(Foresight
+ PRIVATE
+ CURRENT_VERSION="${CURRENT_VERSION}"
PUBLIC
# JUCE_WEB_BROWSER and JUCE_USE_CURL would be on by default, but you might not need them.
JUCE_WEB_BROWSER=0 # If you remove this, add `NEEDS_WEB_BROWSER TRUE` to the `juce_add_plugin` call
@@ -53,20 +66,14 @@ target_compile_definitions(FORESIGHT
JUCE_VST3_CAN_REPLACE_VST2=0
)
-target_link_libraries(FORESIGHT
+target_link_libraries(Foresight
PRIVATE
- juce::juce_audio_basics
- juce::juce_audio_devices
- juce::juce_audio_formats
juce::juce_audio_plugin_client
- juce::juce_audio_processors
- juce::juce_audio_utils
juce::juce_core
juce::juce_data_structures
juce::juce_events
juce::juce_graphics
juce::juce_gui_basics
- juce::juce_gui_extra
PUBLIC
juce::juce_recommended_config_flags
juce::juce_recommended_lto_flags
diff --git a/README.md b/README.md
index 49ca17b..10dec2c 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,331 @@
-# Foresight
\ No newline at end of file
+# Foresight
+
+Foresight is a MIDI plugin that can offset MIDI notes by a configurable amount. This is useful when using sampled acoustic instruments, as the start of most audio samples are not what we perceive as the start of the note, or the transient. Foresight is especially useful when you want to be able to switch between multiple articulations in one track in your DAW, as Foresight can dynamically switch the MIDI note delay depending on what articulation is in use.
+
+Note that currently Foresight is currently strictly monophonic. Notes in the specified playable range will cut each other off when more than one note is played at once.
+
+## Configuration
+
+```xml
+
+```
+
+Foresight's configuration uses XML. It must start with the version of the configuration format and the name of the configuration.
+
+### Settings section (``)
+
+#### Range tag (``)
+
+```xml
+note
+```
+
+The `range` tag specifies what notes Foresight will process. Any notes outside of this range will be ignored and passed through as normal. The `lower` or `upper` boundaries together specifies a group of notes to process. For example, this will specify that only notes `A1-G5` should be processed:
+
+```xml
+A1
+G5
+```
+
+#### Keyswitches tag (``)
+
+```xml
+note
+```
+
+The `keyswitches` tag operates exactly like the `range` tag, except it specifies what notes will be processed as keyswitches.
+
+#### Block tag (``)
+
+```xml
+value
+```
+
+The `block` tag stops the specified CC or note (outside of what is specified in the `range` tags) from being passed to the output. This does not affect messages generated by nodes in the `output` section. A value starting with `CC` specifies a CC, anything else specifies a note, such as `C#3`.
+
+```xml
+CC3
+D#2
+```
+
+### Input section (` `)
+
+The `input` section specifies the kinds of articulations this config will handle, and what triggers each articulation. `switch` nodes start a decision tree, and `case` nodes can match depending on the input. `switch` nodes are evaluated top to bottom, and multiple `case` nodes can be matched.
+
+#### Switch node (``)
+
+```xml
+articulation
+```
+
+`switch` nodes can have many different types of targets, and the value of the target is matched by `case` nodes:
+
+- Starts with `KS_`:
+ - Note that is inside of the `keyswitches` tags. Has no value.
+ - Ex: `KS_C0`
+- Starts with `CC`:
+ - Value is the current value of the CC.
+ - Ex: `CC1`
+- Note name:
+ - Held note outside of the `range` tags. Value is the note's velocity.
+ - Ex: `G3`
+- `legato`
+ - Whether the current note is part of a legato phrase.
+- `velocity`
+ - The velocity of the current note.
+- `length`
+ - The length of the current note.
+- `program`
+ - The currently active MIDI program. Starts at 0, though some DAWs incorrectly report the first MIDI program as 1.
+
+The articulation name specified here must have a corresponding `output` node as well.
+
+#### Case node (``)
+
+```xml
+
+
+
+
+```
+
+`case` nodes compare the target of the parent `switch` node to a value or range of values. All child nodes are ignored if the condition doesn't match.
+
+- `equals` matches if the parent node's target matches the specified value.
+- `less` matches if the parent node's target is less than the specified value.
+- `greater` matches if the parent node's target is greater than the specified value.
+
+### Output section (``)
+
+The `output` section controls what happens when an articulation is matched by a `input` node.
+
+#### Tag node (``)
+
+```xml
+
+```
+
+`tag` nodes specify what articulation from the `input` section will be processed.
+
+#### Set node (``)
+
+```xml
+value
+```
+
+`set` nodes specify what to do with a parent `tag` node's articulation. The `target` can be:
+
+- `start`
+ - Controls the amount of start delay on the current node, in milliseconds. Can be positive or negative.
+ - Ex. `-150 `
+- `end`
+ - Controls the amount of ending delay on the current node, in milliseconds. Can be positive or negative.
+ - Ex. `75 `
+- `legato`
+ - Allow the current articulation to start or be part of a legato phrase.
+ - Ex. ` `
+- `note`
+ - Play a new note.
+ - Ex. `F3 `
+- Starts with `CC`:
+ - Send a CC message.
+ - Ex. `127 `
+- `program`
+ - Change the active MIDI program.
+ - Ex. `3 `
+
+### Example configs
+
+#### Orchestral Tools Berlin Orchestra Violins 1
+
+```xml
+
+
+ C1
+ A1
+ G2
+ D6
+
+
+
+
+ Sustain
+ Legato
+
+
+ Sustain
+ Staccato
+ Spiccato
+ Pizzicato
+ Tremolo
+ Trill
+
+
+
+
+ -150
+
+
+
+ -50
+
+
+ -30
+
+
+ -30
+
+
+ -20
+
+
+ -140
+
+
+ -80
+
+
+
+```
+
+#### Cinematic Studio Strings
+
+```xml
+
+
+ C1
+ C7
+ CC58
+
+
+
+
+
+
+
+ LowLatencySlowLegato
+ LowLatencyFastLegato
+
+
+ LowLatencySustain
+
+
+
+
+
+
+ SlowLegato
+ MediumLegato
+ FastLegato
+
+
+ Sustain
+
+
+ Spiccato
+ Staccatissimo
+ Staccato
+ Sforzando
+ Pizzicato
+ BartokSnap
+ ColLegno
+ Trills
+ Harmonics
+ Tremolo
+ MeasuredTremolo
+ MarcatoWithoutOverlay
+ MarcatoWithOverlay
+
+
+
+
+
+ -50
+ 0
+
+
+
+ -150
+ 0
+
+
+
+ -50
+ 0
+
+
+
+ -50
+ 6
+
+
+
+ -333
+ -90
+ 6
+
+
+
+ -250
+ 6
+
+
+
+ -110
+ 6
+
+
+ -60
+ 11
+
+
+ -60
+ 16
+
+
+ -60
+ 21
+
+
+ -60
+ 26
+
+
+ -60
+ 31
+
+
+ -60
+ 36
+
+
+ -60
+ 41
+
+
+ -50
+ 46
+
+
+ -120
+ 51
+
+
+ -50
+ 56
+
+
+ -50
+ 61
+
+
+ -50
+ 66
+
+
+ -50
+ 71
+
+
+
+```
diff --git a/Source/BufferedMidiMessage.h b/Source/BufferedMidiMessage.h
index 4142e3e..591399e 100644
--- a/Source/BufferedMidiMessage.h
+++ b/Source/BufferedMidiMessage.h
@@ -5,7 +5,7 @@
class BufferedMidiMessage {
public:
BufferedMidiMessage(const juce::MidiMessage& message, unsigned long long time) : message(message), time(time) {}
- ~BufferedMidiMessage() {}
+ ~BufferedMidiMessage() = default;
juce::MidiMessage message;
unsigned long long time;
diff --git a/Source/BufferedNote.cpp b/Source/BufferedNote.cpp
index 71fc8c7..eb85308 100644
--- a/Source/BufferedNote.cpp
+++ b/Source/BufferedNote.cpp
@@ -18,7 +18,7 @@ std::optional BufferedNote::length()
}
}
-bool BufferedNote::hasEnd()
+bool BufferedNote::hasEnd() const
{
return endTime.has_value();
}
\ No newline at end of file
diff --git a/Source/BufferedNote.h b/Source/BufferedNote.h
index d5e2e91..1c64f7f 100644
--- a/Source/BufferedNote.h
+++ b/Source/BufferedNote.h
@@ -7,15 +7,15 @@ class BufferedNote {
BufferedNote(const juce::MidiMessage& noteOnMessage, unsigned long long startTime);
BufferedNote(int pitch, int velocity, unsigned long long startTime, unsigned long long endTime) : pitch(pitch), velocity(velocity), startTime(startTime), endTime(endTime) {}
BufferedNote(int pitch, int velocity, unsigned long long startTime) : pitch(pitch), velocity(velocity), startTime(startTime), endTime(std::optional()) {}
- ~BufferedNote() {}
+ ~BufferedNote() = default;
+
+ std::optional length();
+ bool hasEnd() const;
int pitch;
int velocity;
unsigned long long startTime;
int endDelay = 0;
std::optional endTime;
- std::optional length();
- bool hasEnd();
-
bool allowLegato = false;
};
\ No newline at end of file
diff --git a/Source/ConfigParserUtil.cpp b/Source/ConfigParserUtil.cpp
index 20cb474..69eee17 100644
--- a/Source/ConfigParserUtil.cpp
+++ b/Source/ConfigParserUtil.cpp
@@ -1,10 +1,12 @@
#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" };
- int keyNumber, octave = 0, numPos = keyName.indexOfAnyOf("01234567890-");
+ int keyNumber = -1;
+ int octave = 0;
+ int numPos = keyName.indexOfAnyOf("01234567890-");
if (numPos == 0)
keyNumber = keyName.getIntValue(); //apparently already a number!
@@ -20,12 +22,11 @@ int ConfigParserUtil::keyNameToNumber(const juce::String& keyName, const int oct
numPos = keyName.length();
}
- juce::String name(keyName.substring(0, numPos).trim().toUpperCase());
-
+ const juce::String name(keyName.substring(0, numPos).trim().toUpperCase());
keyNumber = juce::StringArray(noteNames, 12).indexOf(name) % 12;
if (keyNumber < 0) {
- throw std::exception("Encountered invalid note name in tag.");
+ throw std::runtime_error("Encountered invalid note name in tag.");
}
}
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..141c821 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 ")
{
}
@@ -11,42 +11,57 @@ Configuration::Configuration(const std::string& xml)
this->xml = xml;
std::unique_ptr xmlDocument = std::make_unique(xml);
- std::unique_ptr rootElement = xmlDocument->getDocumentElementIfTagMatches("foresight");
+ const std::unique_ptr rootElement = xmlDocument->getDocumentElementIfTagMatches("foresight");
// Header
- if (!rootElement) throw std::exception(xmlDocument->getLastParseError().toStdString().c_str());
+ if (!rootElement) throw std::runtime_error(xmlDocument->getLastParseError().toStdString().c_str());
version = rootElement->getIntAttribute("version", 0);
name = rootElement->getStringAttribute("name").toStdString();
- if (version > CURRENT_CONFIG_VERSION) throw std::exception("This configuration was created for a newer version of Foresight. Please update the plugin to use this configuration.");
+ if (version > CURRENT_CONFIG_VERSION) throw std::runtime_error("This configuration was created for a newer version of Foresight. Please update the plugin to use this configuration.");
// Settings
-
juce::XmlElement* settingsRootElement = rootElement->getChildByName("settings");
- if (settingsRootElement) {
+ if (settingsRootElement != nullptr) {
// Latency
juce::XmlElement* latencySettingElement = settingsRootElement->getChildByName("latency");
- if (latencySettingElement) latency = std::stod(latencySettingElement->getAllSubText().toStdString()) / 1000.0;
+ if (latencySettingElement != nullptr) latency = std::stod(latencySettingElement->getAllSubText().toStdString()) / 1000.0;
// Range
for (const auto& rangeElement : settingsRootElement->getChildWithTagNameIterator("range")) {
- std::string rangeModeText = rangeElement->getStringAttribute("boundary", "lower").toStdString();
+ const std::string rangeModeText = rangeElement->getStringAttribute("boundary", "lower").toStdString();
int* targetVariable = rangeModeText == "upper" ? &rangeUpperBoundary : &rangeLowerBoundary;
- int noteNumber = ConfigParserUtil::keyNameToNumber(rangeElement->getAllSubText(), 3);
+ const int noteNumber = ConfigParserUtil::keyNameToNumber(rangeElement->getAllSubText());
*targetVariable = noteNumber;
+
+#ifdef DEBUG
+ DBG("range" << rangeModeText << " : " << noteNumber);
+#endif
+ }
+
+ // Keyswitches
+ for (const auto& keyswitchElement : settingsRootElement->getChildWithTagNameIterator("keyswitches")) {
+ const std::string keyswitchModeText = keyswitchElement->getStringAttribute("boundary", "lower").toStdString();
+ int* targetVariable = keyswitchModeText == "upper" ? &keyswitchUpperBoundary : &keyswitchLowerBoundary;
+ const int noteNumber = ConfigParserUtil::keyNameToNumber(keyswitchElement->getAllSubText());
+ *targetVariable = noteNumber;
+
+#ifdef DEBUG
+ DBG("keyswitches" << keyswitchModeText << " : " << noteNumber);
+#endif
}
// Blocklist
for (const auto& blockElement : settingsRootElement->getChildWithTagNameIterator("block")) {
- juce::String targetText = blockElement->getAllSubText();
+ const juce::String targetText = blockElement->getAllSubText();
if (targetText.startsWith("CC")) {
blocked.insert(targetText.trim().toStdString());
}
else {
- blocked.insert(std::to_string(ConfigParserUtil::keyNameToNumber(targetText, 3)));
+ blocked.insert(std::to_string(ConfigParserUtil::keyNameToNumber(targetText)));
}
}
}
@@ -55,7 +70,7 @@ Configuration::Configuration(const std::string& xml)
juce::XmlElement* inputTreeRootElement = rootElement->getChildByName("input");
- if (!inputTreeRootElement) throw std::exception("No node found.");
+ if (inputTreeRootElement == nullptr) throw std::runtime_error("No node found.");
inputTreeRoot = std::make_unique(*inputTreeRootElement);
@@ -63,33 +78,45 @@ Configuration::Configuration(const std::string& xml)
juce::XmlElement* outputListRootElement = rootElement->getChildByName("output");
- if (!outputListRootElement) throw std::exception("No node found.");
+ if (outputListRootElement == nullptr) throw std::runtime_error("No node found.");
+ int maxOutputStartDelay = 0;
for (const auto& tagElement : outputListRootElement->getChildIterator()) {
- std::string tagName = tagElement->getStringAttribute("name").toStdString();
+ const std::string tagName = tagElement->getStringAttribute("name").toStdString();
for (const auto& setElement : tagElement->getChildIterator()) {
- outputList[tagName].emplace_back(*setElement);
+ auto outputNode = OutputListNode(*setElement);
+ if (outputNode.getTargetType() == OutputListNode::START && std::abs(outputNode.getValueRaw()) > maxOutputStartDelay) {
+ maxOutputStartDelay = std::abs(outputNode.getValueRaw());
+ }
+
+ outputList[tagName].emplace_back(outputNode);
}
}
+
+ // if the latency isn't set, set it to the absolute value of the
+ // greatest 'start' target of the output nodes
+ if (latency == 0.0 && maxOutputStartDelay != 0) {
+ latency = maxOutputStartDelay / 1000.0;
+ }
}
-std::string Configuration::getSourceXML()
+std::string Configuration::getSourceXML() const
{
return xml;
}
-std::string Configuration::getName()
+std::string Configuration::getName() const
{
return name;
}
-double Configuration::getLatencySeconds()
+double Configuration::getLatencySeconds() const
{
return latency;
}
-double Configuration::getSampleRate()
+double Configuration::getSampleRate() const
{
return lastSampleRate;
}
@@ -99,20 +126,25 @@ void Configuration::updateSampleRate(double sampleRate)
lastSampleRate = sampleRate;
}
-int Configuration::getLatencySamples()
+int Configuration::getLatencySamples() const
{
- return (int)std::round(latency * lastSampleRate);
+ return static_cast(std::round(latency * lastSampleRate));
}
-bool Configuration::isInRange(int noteNumber)
+bool Configuration::isInRange(int noteNumber) const
{
return noteNumber >= rangeLowerBoundary && noteNumber <= rangeUpperBoundary;
}
-bool Configuration::isBlocked(const juce::MidiMessage& message)
+bool Configuration::isKeyswitch(int noteNumber) const
+{
+ return noteNumber >= keyswitchLowerBoundary && noteNumber <= keyswitchUpperBoundary;
+}
+
+bool Configuration::isBlocked(const juce::MidiMessage& message) const
{
if (message.isController()) {
- std::string ccString = "CC" + std::to_string(message.getControllerNumber());
+ const std::string ccString = "CC" + std::to_string(message.getControllerNumber());
return blocked.contains(ccString);
}
else if (message.isNoteOnOrOff()) {
@@ -122,7 +154,7 @@ bool Configuration::isBlocked(const juce::MidiMessage& message)
return false;
}
-std::unordered_set Configuration::getTagsForNote(NoteContext& context)
+std::unordered_set Configuration::getTagsForNote(NoteContext& context) const
{
inputTreeRoot->visit(context);
return context.getTags();
diff --git a/Source/Configuration.h b/Source/Configuration.h
index 459e2f4..5ae0768 100644
--- a/Source/Configuration.h
+++ b/Source/Configuration.h
@@ -8,17 +8,18 @@
class Configuration {
public:
Configuration();
- Configuration(const std::string& xml);
- std::string getSourceXML();
- std::string getName();
- double getLatencySeconds();
- double getSampleRate();
+ explicit Configuration(const std::string& xml);
+ std::string getSourceXML() const;
+ std::string getName() const;
+ double getLatencySeconds() const;
+ double getSampleRate() const;
void updateSampleRate(double sampleRate);
- int getLatencySamples();
- bool isInRange(int noteNumber);
- bool isBlocked(const juce::MidiMessage& message);
+ int getLatencySamples() const;
+ bool isInRange(int noteNumber) const;
+ bool isKeyswitch(int noteNumber) const;
+ bool isBlocked(const juce::MidiMessage& message) const;
- std::unordered_set getTagsForNote(NoteContext& context);
+ std::unordered_set getTagsForNote(NoteContext& context) const;
std::vector getOutputNodes(const std::string& tag);
private:
int version;
@@ -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/GuiMainComponent.cpp b/Source/GuiMainComponent.cpp
index 14e0289..81e6fb2 100644
--- a/Source/GuiMainComponent.cpp
+++ b/Source/GuiMainComponent.cpp
@@ -33,14 +33,16 @@ GuiMainComponent::GuiMainComponent ()
//[/Constructor_pre]
setName ("MainComponent");
- juce_currentConfigValue_label.reset (new juce::Label ("currentConfigValue",
- TRANS("Config Title")));
- addAndMakeVisible (juce_currentConfigValue_label.get());
- juce_currentConfigValue_label->setFont (juce::Font (24.00f, juce::Font::plain).withTypefaceStyle ("Regular"));
- juce_currentConfigValue_label->setJustificationType (juce::Justification::centred);
- juce_currentConfigValue_label->setEditable (false, false, false);
- juce_currentConfigValue_label->setColour (juce::TextEditor::textColourId, juce::Colours::black);
- juce_currentConfigValue_label->setColour (juce::TextEditor::backgroundColourId, juce::Colour (0x00000000));
+
+ juce_pluginVersion_label.reset (new juce::Label ("pluginVersion",
+ TRANS("Plugin Version")));
+ addAndMakeVisible (juce_pluginVersion_label.get());
+ juce_pluginVersion_label->setFont (juce::Font (14.00f, juce::Font::plain).withTypefaceStyle ("Regular"));
+ juce_pluginVersion_label->setJustificationType (juce::Justification::centred);
+ juce_pluginVersion_label->setEditable (false, false, false);
+ juce_pluginVersion_label->setColour (juce::TextEditor::textColourId, juce::Colours::black);
+ juce_pluginVersion_label->setColour (juce::TextEditor::backgroundColourId, juce::Colour (0x00000000));
+ juce_pluginVersion_label->setText (CURRENT_VERSION, juce::NotificationType::dontSendNotification);
juce_currentConfigHeading_label.reset (new juce::Label ("currentConfigHeading",
TRANS("Loaded Configuration")));
@@ -51,6 +53,15 @@ GuiMainComponent::GuiMainComponent ()
juce_currentConfigHeading_label->setColour (juce::TextEditor::textColourId, juce::Colours::black);
juce_currentConfigHeading_label->setColour (juce::TextEditor::backgroundColourId, juce::Colour (0x00000000));
+ juce_currentConfigValue_label.reset (new juce::Label ("currentConfigValue",
+ TRANS("Config Title")));
+ addAndMakeVisible (juce_currentConfigValue_label.get());
+ juce_currentConfigValue_label->setFont (juce::Font (24.00f, juce::Font::plain).withTypefaceStyle ("Regular"));
+ juce_currentConfigValue_label->setJustificationType (juce::Justification::centred);
+ juce_currentConfigValue_label->setEditable (false, false, false);
+ juce_currentConfigValue_label->setColour (juce::TextEditor::textColourId, juce::Colours::black);
+ juce_currentConfigValue_label->setColour (juce::TextEditor::backgroundColourId, juce::Colour (0x00000000));
+
juce_currentLatency_label.reset (new juce::Label ("currentLatency",
TRANS("Total latency: xxx ms")));
addAndMakeVisible (juce_currentLatency_label.get());
@@ -76,8 +87,9 @@ GuiMainComponent::~GuiMainComponent()
//[Destructor_pre]. You can add your own custom destruction code here..
//[/Destructor_pre]
- juce_currentConfigValue_label = nullptr;
+ juce_pluginVersion_label = nullptr;
juce_currentConfigHeading_label = nullptr;
+ juce_currentConfigValue_label = nullptr;
juce_currentLatency_label = nullptr;
@@ -102,8 +114,9 @@ void GuiMainComponent::resized()
//[UserPreResize] Add your own custom resize code here..
//[/UserPreResize]
- juce_currentConfigValue_label->setBounds (proportionOfWidth (0.0000f), (getHeight() / 2) + -30, proportionOfWidth (1.0000f), 24);
+ juce_pluginVersion_label->setBounds (proportionOfWidth (0.0000f), (getHeight() / 2) + -88, proportionOfWidth (1.0000f), 24);
juce_currentConfigHeading_label->setBounds (proportionOfWidth (0.0000f), (getHeight() / 2) + -54, proportionOfWidth (1.0000f), 24);
+ juce_currentConfigValue_label->setBounds (proportionOfWidth (0.0000f), (getHeight() / 2) + -30, proportionOfWidth (1.0000f), 24);
juce_currentLatency_label->setBounds (proportionOfWidth (0.0000f), (getHeight() / 2) + 34, proportionOfWidth (1.0000f), 24);
//[UserResized] Add your own custom resize handling here..
//[/UserResized]
diff --git a/Source/GuiMainComponent.h b/Source/GuiMainComponent.h
index 41bac91..ef68d76 100644
--- a/Source/GuiMainComponent.h
+++ b/Source/GuiMainComponent.h
@@ -56,6 +56,7 @@ class GuiMainComponent : public juce::Component
//[/UserVariables]
//==============================================================================
+ std::unique_ptr juce_pluginVersion_label;
std::unique_ptr juce_currentConfigValue_label;
std::unique_ptr juce_currentConfigHeading_label;
std::unique_ptr juce_currentLatency_label;
diff --git a/Source/IInputTreeNode.h b/Source/IInputTreeNode.h
index d97c4eb..9801d86 100644
--- a/Source/IInputTreeNode.h
+++ b/Source/IInputTreeNode.h
@@ -3,6 +3,6 @@
class IInputTreeNode {
public:
- ~IInputTreeNode() {}
+ virtual ~IInputTreeNode() = default;
virtual NoteContext& visit(NoteContext& context) = 0;
};
\ No newline at end of file
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/InputTreeNodeFactory.cpp b/Source/InputTreeNodeFactory.cpp
index db5b9cf..69a7435 100644
--- a/Source/InputTreeNodeFactory.cpp
+++ b/Source/InputTreeNodeFactory.cpp
@@ -4,13 +4,13 @@
IInputTreeNode* InputTreeNodeFactory::make(const juce::XmlElement& source)
{
- IInputTreeNode* node;
+ IInputTreeNode* node = nullptr;
if (source.getTagName() == "switch") {
node = new InputTreeSwitchNode(source);
}
else if (source.getTagName() == "case") {
- throw std::exception("Encountered a node that is not a direct child of a node.");
+ throw std::runtime_error("Encountered a node that is not a direct child of a node.");
}
else {
node = new InputTreeTagNode(source);
diff --git a/Source/InputTreeRootNode.h b/Source/InputTreeRootNode.h
index 766d02c..8abed91 100644
--- a/Source/InputTreeRootNode.h
+++ b/Source/InputTreeRootNode.h
@@ -6,7 +6,7 @@
class InputTreeRootNode : public IInputTreeNode {
public:
- InputTreeRootNode(const juce::XmlElement& source);
+ explicit InputTreeRootNode(const juce::XmlElement& source);
NoteContext& visit(NoteContext& context) override;
private:
std::vector> children;
diff --git a/Source/InputTreeSwitchNode.cpp b/Source/InputTreeSwitchNode.cpp
index 187bd01..9135bd0 100644
--- a/Source/InputTreeSwitchNode.cpp
+++ b/Source/InputTreeSwitchNode.cpp
@@ -5,36 +5,51 @@
InputTreeSwitchNode::InputTreeSwitchNode(const juce::XmlElement& source)
{
if (!source.hasAttribute("target")) {
- throw std::exception("Encountered a node without the required target attribute.");
+ throw std::runtime_error("Encountered a node without the required target attribute.");
}
- std::string targetStr = source.getStringAttribute("target").toStdString();
+ const std::string targetStr = source.getStringAttribute("target").toStdString();
if (targetStr == "legato") target = LEGATO;
else if (targetStr == "velocity") target = VELOCITY;
else if (targetStr == "length") target = LENGTH;
else if (targetStr == "program") target = PROGRAM;
else {
if (targetStr.starts_with("CC")) {
- std::string trimmed = targetStr.substr(2, targetStr.length());
+ const std::string trimmed = targetStr.substr(2, targetStr.length());
targetNumber = std::stoi(trimmed);
target = CC;
+ }
+ else if (targetStr.starts_with("KS_")) {
+ const std::string trimmed = targetStr.substr(3, targetStr.length());
+ try {
+ targetNumber = ConfigParserUtil::keyNameToNumber(trimmed);
+ target = KEYSWITCH;
+ }
+ catch (...) {
+ throw std::runtime_error("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) {
- throw std::exception("Encountered a node target attribute with an invalid value.");
+ catch (...) {
+ throw std::runtime_error("Encountered a node target attribute with an invalid value.");
}
}
}
+ 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>()));
+ auto insertIndex = children.size();
+ children.emplace_back(std::make_tuple(InputTreeCase(*caseEntryElement, keyswitch), std::vector>()));
for (const auto& caseChildElement : caseEntryElement->getChildIterator()) {
IInputTreeNode* child = InputTreeNodeFactory::make(*caseChildElement);
@@ -42,8 +57,8 @@ InputTreeSwitchNode::InputTreeSwitchNode(const juce::XmlElement& source)
}
}
else {
- int insertIndex = children.size();
- children.emplace_back(std::make_tuple(InputTreeCase(*caseEntryElement), std::vector>()));
+ auto insertIndex = children.size();
+ children.emplace_back(std::make_tuple(InputTreeCase(*caseEntryElement, keyswitch), std::vector>()));
IInputTreeNode* child = InputTreeNodeFactory::make(*caseEntryElement);
std::get<1>(children[insertIndex]).emplace_back(child);
@@ -74,10 +89,13 @@ int InputTreeSwitchNode::getTargetValue(NoteContext& context)
return context.getVelocity();
break;
case LEGATO:
- return context.isLegato();
+ return static_cast(context.isLegato());
break;
case LENGTH:
- return context.getLength().has_value() ? context.getLength().value() % INT_MAX : INT_MAX;
+ if (!context.getLength().has_value()) {
+ return INT_MAX;
+ }
+ return static_cast(context.getLength().value()) % INT_MAX;
break;
case CC:
return context.getCCValue(targetNumber);
@@ -85,8 +103,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::runtime_error("Invalid target value");
}
}
diff --git a/Source/InputTreeSwitchNode.h b/Source/InputTreeSwitchNode.h
index c6d00cf..e61cc0a 100644
--- a/Source/InputTreeSwitchNode.h
+++ b/Source/InputTreeSwitchNode.h
@@ -6,10 +6,10 @@
class InputTreeSwitchNode : public IInputTreeNode {
public:
- InputTreeSwitchNode(const juce::XmlElement& source);
+ explicit 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/InputTreeTagNode.h b/Source/InputTreeTagNode.h
index a5a7946..1996396 100644
--- a/Source/InputTreeTagNode.h
+++ b/Source/InputTreeTagNode.h
@@ -5,7 +5,7 @@
class InputTreeTagNode : public IInputTreeNode {
public:
- InputTreeTagNode(const juce::XmlElement& source);
+ explicit InputTreeTagNode(const juce::XmlElement& source);
NoteContext& visit(NoteContext& context) override;
private:
std::string tag;
diff --git a/Source/NoteContext.cpp b/Source/NoteContext.cpp
index 65712c9..2d6a653 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, std::array ccStates, std::array heldNotes, 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,8 +37,13 @@ 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;
+ return previousNote.has_value() && previousNote->allowLegato && note->startTime - previousNote->endTime.value() <= 64;
}
std::optional NoteContext::getLength() const
diff --git a/Source/NoteContext.h b/Source/NoteContext.h
index 0da0669..c0edeef 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, std::array ccStates, std::array heldNotes, 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;
@@ -22,7 +23,8 @@ class NoteContext {
BufferedNote* note;
std::optional previousNote;
std::unordered_set tags;
- int* ccStates;
- int* heldNotes;
+ std::array ccStates;
+ std::array heldNotes;
int program;
+ int lastKeyswitch;
};
\ No newline at end of file
diff --git a/Source/NoteProcessor.cpp b/Source/NoteProcessor.cpp
index 85aeede..5cb022b 100644
--- a/Source/NoteProcessor.cpp
+++ b/Source/NoteProcessor.cpp
@@ -13,7 +13,7 @@ NoteProcessor::NoteProcessor(const NoteContext& note, Configuration* configurati
addBeforeNote(juce::MidiMessage::controllerEvent(channel, node.getCCNumber(), node.getValue(note)));
break;
case OutputListNode::NOTE:
- addBeforeNote(juce::MidiMessage::noteOn(channel, node.getValue(note), juce::uint8(64)));
+ addBeforeNote(juce::MidiMessage::noteOn(channel, node.getValue(note), static_cast(64)));
addAfterNote(juce::MidiMessage::noteOff(channel, node.getValue(note)));
break;
case OutputListNode::PROGRAM:
@@ -33,14 +33,14 @@ NoteProcessor::NoteProcessor(const NoteContext& note, Configuration* configurati
}
}
-void NoteProcessor::addStartDelay(double delay)
+void NoteProcessor::addStartDelay(int delay)
{
- startDelaySamples += (delay / 1000) * sampleRate;
+ startDelaySamples += static_cast((static_cast(delay) / 1000.0) * sampleRate);
}
-void NoteProcessor::addEndDelay(double delay)
+void NoteProcessor::addEndDelay(int delay)
{
- endDelaySamples += (delay / 1000) * sampleRate;
+ endDelaySamples += static_cast((static_cast(delay) / 1000.0) * sampleRate);
}
void NoteProcessor::addBeforeNote(juce::MidiMessage message)
@@ -60,12 +60,12 @@ void NoteProcessor::applyStartDelay()
target->startTime += startDelaySamples;
}
-int NoteProcessor::getStartDelaySamples()
+int NoteProcessor::getStartDelaySamples() const
{
return startDelaySamples;
}
-int NoteProcessor::getEndDelaySamples()
+int NoteProcessor::getEndDelaySamples() const
{
return endDelaySamples;
}
@@ -80,7 +80,7 @@ std::vector NoteProcessor::getResults()
results.emplace_back(message);
}
- results.emplace_back(juce::MidiMessage::noteOn(channel, target->pitch, (juce::uint8)target->velocity));
+ results.emplace_back(juce::MidiMessage::noteOn(channel, target->pitch, static_cast(target->velocity)));
for (const auto& message : afterNoteMessages) {
results.emplace_back(message);
diff --git a/Source/NoteProcessor.h b/Source/NoteProcessor.h
index 9d2a3ff..cc74fa2 100644
--- a/Source/NoteProcessor.h
+++ b/Source/NoteProcessor.h
@@ -9,8 +9,8 @@ class NoteProcessor {
NoteProcessor(const NoteContext& note, Configuration* configuration, const std::unordered_set& tags, int channel);
void applyStartDelay();
- int getStartDelaySamples();
- int getEndDelaySamples();
+ int getStartDelaySamples() const;
+ int getEndDelaySamples() const;
std::vector getResults();
private:
@@ -24,8 +24,8 @@ class NoteProcessor {
std::vector beforeNoteMessages;
std::vector afterNoteMessages;
- void addStartDelay(double delay);
- void addEndDelay(double delay);
+ void addStartDelay(int delay);
+ void addEndDelay(int delay);
void addBeforeNote(juce::MidiMessage message);
void addAfterNote(juce::MidiMessage message);
diff --git a/Source/OutputListNode.cpp b/Source/OutputListNode.cpp
index a9ea28e..21c4373 100644
--- a/Source/OutputListNode.cpp
+++ b/Source/OutputListNode.cpp
@@ -9,8 +9,8 @@ OutputListNode::OutputListNode()
OutputListNode::OutputListNode(const juce::XmlElement& source)
{
- std::string targetStr = source.getStringAttribute("target").toStdString();
- std::string valueStr = source.getAllSubText().toStdString();
+ const std::string targetStr = source.getStringAttribute("target").toStdString();
+ const std::string valueStr = source.getAllSubText().toStdString();
if (targetStr == "note") target = NOTE;
else if (targetStr == "start") target = START;
@@ -19,32 +19,32 @@ OutputListNode::OutputListNode(const juce::XmlElement& source)
else if (targetStr == "program") target = PROGRAM;
else {
if (targetStr.starts_with("CC")) {
- std::string trimmed = targetStr.substr(2, targetStr.length());
+ const std::string trimmed = targetStr.substr(2, targetStr.length());
ccNumber = std::stoi(trimmed);
target = CC;
}
else {
- throw std::exception("Encountered a node target attribute with an invalid value.");
+ throw std::runtime_error("Encountered a node target attribute with an invalid value.");
}
}
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")) {
- int multiplierStartIndex = valueStr.find('*');
+ auto multiplierStartIndex = valueStr.find('*');
if (multiplierStartIndex > 0) {
- std::string trimmed = valueStr.substr(2, valueStr.length());
+ const std::string trimmed = valueStr.substr(2, valueStr.length());
readCC = std::stoi(trimmed);
- std::string multiplierStr = valueStr.substr(multiplierStartIndex + 1, valueStr.length());
+ const std::string multiplierStr = valueStr.substr(multiplierStartIndex + 1, valueStr.length());
readValueMultiplier = std::stof(multiplierStr);
}
else {
- std::string trimmed = valueStr.substr(2, valueStr.length());
+ const std::string trimmed = valueStr.substr(2, valueStr.length());
readCC = std::stoi(trimmed);
}
}
@@ -54,12 +54,12 @@ OutputListNode::OutputListNode(const juce::XmlElement& source)
}
}
-OutputListNode::TargetType OutputListNode::getTargetType()
+OutputListNode::TargetType OutputListNode::getTargetType() const
{
return target;
}
-int OutputListNode::getCCNumber()
+int OutputListNode::getCCNumber() const
{
return ccNumber;
}
@@ -67,10 +67,14 @@ int OutputListNode::getCCNumber()
int OutputListNode::getValue(const NoteContext& context)
{
if (readCC.has_value()) {
- int output = std::round(context.getCCValue(readCC.value()) * readValueMultiplier.value_or(1.0f));
- return output;
+ return static_cast(std::round(context.getCCValue(readCC.value()) * readValueMultiplier.value_or(1.0f)));
}
else {
return value;
}
+}
+
+int OutputListNode::getValueRaw() const
+{
+ return value;
}
\ No newline at end of file
diff --git a/Source/OutputListNode.h b/Source/OutputListNode.h
index 0ae924a..86daae7 100644
--- a/Source/OutputListNode.h
+++ b/Source/OutputListNode.h
@@ -7,12 +7,13 @@
class OutputListNode {
public:
OutputListNode();
- OutputListNode(const juce::XmlElement& source);
+ explicit OutputListNode(const juce::XmlElement& source);
enum TargetType { CC, NOTE, START, END, LEGATO, PROGRAM };
- TargetType getTargetType();
- int getCCNumber();
+ TargetType getTargetType() const;
+ int getCCNumber() const;
int getValue(const NoteContext& context);
+ int getValueRaw() const;
private:
TargetType target;
diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h
index 356aa88..8882467 100644
--- a/Source/PluginEditor.h
+++ b/Source/PluginEditor.h
@@ -19,7 +19,7 @@
class ForesightAudioProcessorEditor : public juce::AudioProcessorEditor
{
public:
- ForesightAudioProcessorEditor (ForesightAudioProcessor&);
+ explicit ForesightAudioProcessorEditor (ForesightAudioProcessor&);
~ForesightAudioProcessorEditor() override;
//==============================================================================
diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp
index 6d5ff57..91af039 100644
--- a/Source/PluginProcessor.cpp
+++ b/Source/PluginProcessor.cpp
@@ -147,69 +147,22 @@ bool ForesightAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts
}
#endif
-void ForesightAudioProcessor::processBlock (juce::AudioBuffer& buffer, juce::MidiBuffer& inputMidi)
+void ForesightAudioProcessor::processBlock(juce::AudioBuffer& buffer, juce::MidiBuffer& inputMidi)
{
- isCurrentlyInsideProcessBlock = true;
-
- auto playheadInfo = getPlayHead()->getPosition();
- bool isPlaying = playheadInfo->getIsPlaying();
-
- buffer.clear();
-
- // Clear everything if it was just stopped or started
- if (!isPlaying && lastPlayingState) {
- clearState();
-
- juce::MidiBuffer noteStopBuffer;
- for (int i = 0; i < 16; i++)
- {
- noteStopBuffer.addEvent(juce::MidiMessage::allNotesOff(i + 1), 0);
- }
-
- inputMidi.swapWith(noteStopBuffer); // Also send note off if it was stopped
- }
- else if (isPlaying && !lastPlayingState) {
- clearState();
- }
-
-#if DEBUG
- if (true)
-#else
- // Bypass all processing if the host is not playing
- if (isPlaying)
-#endif
- {
- // Split the input into a maximum of 16 monophonic voices
- juce::MidiBuffer splitBuffer = voiceManager->processBuffer(inputMidi);
-
- // Process each voice seperately
- juce::MidiBuffer channelBuffers[16];
- for (int i = 0; i < 16; i++)
- {
- channelBuffers[i] = voiceProcessors[i].processBuffer(splitBuffer, i + 1, buffer.getNumSamples(), false);
- }
-
- // Merge the buffers
- juce::MidiBuffer mergedOutputBuffer;
- for (const juce::MidiBuffer& channelBuffer : channelBuffers)
- {
- mergedOutputBuffer.addEvents(channelBuffer, 0, -1, 0);
- }
-
- inputMidi.swapWith(mergedOutputBuffer);
- }
-
- lastPlayingState = isPlaying;
-
- isCurrentlyInsideProcessBlock = false;
+ processMidi(buffer, inputMidi, false);
}
void ForesightAudioProcessor::processBlockBypassed(juce::AudioBuffer& buffer, juce::MidiBuffer& inputMidi)
+{
+ processMidi(buffer, inputMidi, true);
+}
+
+void ForesightAudioProcessor::processMidi(juce::AudioBuffer& buffer, juce::MidiBuffer& inputMidi, bool bypassed)
{
isCurrentlyInsideProcessBlock = true;
auto playheadInfo = getPlayHead()->getPosition();
- bool isPlaying = playheadInfo->getIsPlaying();
+ const bool isPlaying = playheadInfo->getIsPlaying();
buffer.clear();
@@ -217,19 +170,24 @@ void ForesightAudioProcessor::processBlockBypassed(juce::AudioBuffer& buf
if (!isPlaying && lastPlayingState) {
clearState();
+ // Also send note off messages if it was stopped
juce::MidiBuffer noteStopBuffer;
- for (int i = 0; i < 16; i++)
+ for (int channel = 1; channel <= 16; channel++)
{
- noteStopBuffer.addEvent(juce::MidiMessage::allNotesOff(i + 1), 0);
+ for (int note = 0; note < 128; note++)
+ {
+ noteStopBuffer.addEvent(juce::MidiMessage::noteOff(channel, note), 0);
+ }
}
- inputMidi.swapWith(noteStopBuffer); // Also send note off if it was stopped
+ inputMidi.swapWith(noteStopBuffer);
}
else if (isPlaying && !lastPlayingState) {
clearState();
}
-#if DEBUG
+#ifdef DEBUG
+ // JUCE plugin host never reports that it is playing
if (true)
#else
// Bypass all processing if the host is not playing
@@ -239,11 +197,11 @@ void ForesightAudioProcessor::processBlockBypassed(juce::AudioBuffer& buf
// Split the input into a maximum of 16 monophonic voices
juce::MidiBuffer splitBuffer = voiceManager->processBuffer(inputMidi);
- // Process each voice seperately
- juce::MidiBuffer channelBuffers[16];
+ // Process each voice separately
+ std::array channelBuffers;
for (int i = 0; i < 16; i++)
{
- channelBuffers[i] = voiceProcessors[i].processBuffer(splitBuffer, i + 1, buffer.getNumSamples(), true);
+ channelBuffers[i] = voiceProcessors[i].processBuffer(splitBuffer, i + 1, buffer.getNumSamples(), bypassed);
}
// Merge the buffers
@@ -270,7 +228,7 @@ bool ForesightAudioProcessor::hasEditor() const
juce::AudioProcessorEditor* ForesightAudioProcessor::createEditor()
{
isCreatingEditor = true;
- ForesightAudioProcessorEditor* editor = new ForesightAudioProcessorEditor (*this);
+ auto editor = new ForesightAudioProcessorEditor (*this);
isCreatingEditor = false;
return editor;
@@ -288,7 +246,7 @@ void ForesightAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
juce::XmlElement* windowSizeElement = state->createNewChildElement("window");
auto editor = getActiveEditor();
- if (editor) {
+ if (editor != nullptr) {
currentWindowWidth = editor->getWidth();
currentWindowHeight = editor->getHeight();
}
@@ -301,16 +259,16 @@ void ForesightAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
void ForesightAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
{
- std::unique_ptr state(getXmlFromBinary(data, sizeInBytes));
+ const std::unique_ptr state(getXmlFromBinary(data, sizeInBytes));
// Check if the state is valid
if (state && state->getTagName() == "foresight-state" && std::stoi(state->getAttributeValue(0).toStdString()) <= CURRENT_STATE_VERSION)
{
- std::string source = state->getChildByName("source")->getAllSubText().toStdString();
+ const std::string source = state->getChildByName("source")->getAllSubText().toStdString();
setConfiguration(source);
juce::XmlElement* windowSizeElement = state->getChildByName("window");
- if (windowSizeElement) {
+ if (windowSizeElement != nullptr) {
currentWindowWidth = windowSizeElement->getIntAttribute("width");
currentWindowHeight = windowSizeElement->getIntAttribute("height");
}
@@ -381,8 +339,8 @@ Configuration& ForesightAudioProcessor::getConfiguration()
void ForesightAudioProcessor::updateGui()
{
- bool editorAvailable = (getActiveEditor() || isCreatingEditor);
- if (updateGuiCallback && editorAvailable) updateGuiCallback(configuration->getName(), "Latency: " + std::to_string(configuration->getLatencySeconds()) + "s", configuration->getSourceXML());
+ const bool editorAvailable = (getActiveEditor() != nullptr || isCreatingEditor);
+ if (updateGuiCallback != nullptr && editorAvailable) updateGuiCallback(configuration->getName(), "Latency: " + std::to_string(configuration->getLatencySeconds()) + "s", configuration->getSourceXML());
}
void ForesightAudioProcessor::setUpdateGuiCallback(const std::function& callback)
diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h
index c223ebc..0a19e7c 100644
--- a/Source/PluginProcessor.h
+++ b/Source/PluginProcessor.h
@@ -39,6 +39,7 @@ class ForesightAudioProcessor : public juce::AudioProcessor
void processBlock (juce::AudioBuffer&, juce::MidiBuffer&) override;
void processBlockBypassed (juce::AudioBuffer&, juce::MidiBuffer&) override;
+ void processMidi(juce::AudioBuffer&, juce::MidiBuffer&, bool);
//==============================================================================
juce::AudioProcessorEditor* createEditor() override;
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..e749080 100644
--- a/Source/VoiceManager.cpp
+++ b/Source/VoiceManager.cpp
@@ -1,13 +1,5 @@
#include "VoiceManager.h"
-VoiceManager::VoiceManager()
-{
-}
-
-VoiceManager::~VoiceManager()
-{
-}
-
juce::MidiBuffer VoiceManager::processBuffer(const juce::MidiBuffer& buffer)
{
juce::MidiBuffer processedBuffer;
@@ -33,30 +25,22 @@ 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()))) {
- message.setChannel(1);
+ if (!message.isNoteOnOrOff() || (message.isNoteOnOrOff() && !configuration->isInRange(message.getNoteNumber()) && !configuration->isKeyswitch(message.getNoteNumber()))) {
processedBuffer.addEvent(message, time);
}
}
- // ...look for a note-off first
for (auto& message : entry.second)
{
- if (message.isNoteOff() && configuration->isInRange(message.getNoteNumber()) && heldNote.has_value() && heldNote == message.getNoteNumber()) {
+ // ...look for a note-off first
+ if (message.isNoteOff() && (configuration->isInRange(message.getNoteNumber()) || configuration->isKeyswitch(message.getNoteNumber())) && heldNote.has_value() && heldNote == message.getNoteNumber()) {
heldNote.reset();
- message.setChannel(1);
processedBuffer.addEvent(message, time);
-
break;
}
- }
-
- // ...handle note-on
- for (auto& message : entry.second)
- {
- if (message.isNoteOn() && configuration->isInRange(message.getNoteNumber()))
- {
+ // ...handle note-on
+ else 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,12 +49,9 @@ juce::MidiBuffer VoiceManager::processBuffer(const juce::MidiBuffer& buffer)
}
// Play the new note
-
heldNote = message.getNoteNumber();
- message.setChannel(1);
processedBuffer.addEvent(message, time);
-
break;
}
}
@@ -79,7 +60,7 @@ juce::MidiBuffer VoiceManager::processBuffer(const juce::MidiBuffer& buffer)
return processedBuffer;
}
-int VoiceManager::getCurrentVoiceCount()
+int VoiceManager::getCurrentVoiceCount() const
{
return 1;
}
@@ -89,7 +70,7 @@ void VoiceManager::reset()
heldNote.reset();
}
-void VoiceManager::updateConfiguration(Configuration* configuration)
+void VoiceManager::updateConfiguration(Configuration* c)
{
- this->configuration = configuration;
+ this->configuration = c;
}
diff --git a/Source/VoiceManager.h b/Source/VoiceManager.h
index 8e0ab29..7bc2c28 100644
--- a/Source/VoiceManager.h
+++ b/Source/VoiceManager.h
@@ -5,8 +5,8 @@
class VoiceManager {
public:
- VoiceManager();
- ~VoiceManager();
+ VoiceManager() = default;
+ ~VoiceManager() = default;
///
/// Returns a new MidiBuffer containing the events from the input buffer,
/// distributed across MIDI channels so that each channel is strictly monophonic.
@@ -19,14 +19,14 @@ class VoiceManager {
/// Gets the amount of currently playing voices after processing the last buffer.
///
/// The amount of playing voices
- int getCurrentVoiceCount();
+ int getCurrentVoiceCount() const;
///
/// Resets the internal state of the VoiceManager. This should be called when playback is stopped.
///
void reset();
- void updateConfiguration(Configuration* configuration);
+ void updateConfiguration(Configuration* c);
private:
std::optional heldNote;
Configuration* configuration;
diff --git a/Source/VoiceProcessor.cpp b/Source/VoiceProcessor.cpp
index 24d5e84..a133f40 100644
--- a/Source/VoiceProcessor.cpp
+++ b/Source/VoiceProcessor.cpp
@@ -3,10 +3,6 @@
#include "NoteContext.h"
#include "NoteProcessor.h"
-VoiceProcessor::VoiceProcessor()
-{
-}
-
VoiceProcessor::VoiceProcessor(const VoiceProcessor& other)
{
bufferSizeSamples = other.bufferSizeSamples;
@@ -58,7 +54,7 @@ juce::MidiBuffer VoiceProcessor::processBuffer(const juce::MidiBuffer& buffer, i
output = processSample(std::optional>(), channel, bypassed);
}
- for (const auto message : output) {
+ for (const auto& message : output) {
outputBuffer.addEvent(message, i);
}
}
@@ -82,19 +78,19 @@ void VoiceProcessor::reset()
unprocessedBuffer.clear();
}
-void VoiceProcessor::updateConfiguration(Configuration* configuration)
+void VoiceProcessor::updateConfiguration(Configuration* c)
{
reset();
- this->bufferSizeSamples = configuration->getLatencySamples();
- this->configuration = configuration;
+ this->bufferSizeSamples = c->getLatencySamples();
+ this->configuration = c;
}
-unsigned long long VoiceProcessor::getReadPosition()
+unsigned long long VoiceProcessor::getReadPosition() const
{
return readHeadPosition;
}
-unsigned long long VoiceProcessor::getWritePosition()
+unsigned long long VoiceProcessor::getWritePosition() const
{
return readHeadPosition + bufferSizeSamples;
}
@@ -133,10 +129,10 @@ 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);
- std::unordered_set tags = configuration->getTagsForNote(context);
+ NoteContext context = NoteContext(note, previousNoteAtReadPosition, readPositionCCStates, readPositionHeldNotes, readPositionProgram, lastKeyswitch);
+ const std::unordered_set tags = configuration->getTagsForNote(context);
NoteProcessor noteProcessor = NoteProcessor(context, configuration, tags, channel);
- std::vector results = noteProcessor.getResults();
+ const std::vector results = noteProcessor.getResults();
#if DEBUG
DBG("Read Note " << note->pitch << ": ");
@@ -165,21 +161,30 @@ 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
+ else if (message.isNoteOff() && heldNoteAtWritePosition != nullptr) { // Note off
heldNoteAtWritePosition->endTime = getWritePosition();
lastWrittenNote = *heldNoteAtWritePosition;
previousNoteAtWritePosition = *heldNoteAtWritePosition;
heldNoteAtWritePosition = nullptr;
}
else if (message.isNoteOn()) { // Note on
- BufferedNote* newNote = new BufferedNote(message, getWritePosition());
+ auto newNote = new BufferedNote(message, getWritePosition());
bufferedNotes.emplace_back(newNote);
heldNoteAtWritePosition = newNote;
// Process new note - only start delay is processed here
- NoteContext context = NoteContext(newNote, previousNoteAtWritePosition, writePositionCCStates, writePositionHeldNotes, writePositionProgram);
- std::unordered_set tags = configuration->getTagsForNote(context);
+ NoteContext context = NoteContext(newNote, previousNoteAtWritePosition, writePositionCCStates, writePositionHeldNotes, writePositionProgram, lastKeyswitch);
+ const std::unordered_set tags = configuration->getTagsForNote(context);
NoteProcessor noteProcessor = NoteProcessor(context, configuration, tags, channel);
noteProcessor.applyStartDelay();
@@ -187,7 +192,7 @@ std::vector VoiceProcessor::processSample(const std::optional
if (std::abs(noteProcessor.getStartDelaySamples()) > 0) {
for (auto& unprocessedMsg : unprocessedBuffer) { // ugh
// 512 samples or about 10ms at 48000kHz is a very generous window, so it's probably even somewhat playable
- if (std::llabs(getWritePosition() - unprocessedMsg.time) < 512) {
+ if (getWritePosition() - unprocessedMsg.time < 512) {
unprocessedMsg.time = newNote->startTime;
}
}
diff --git a/Source/VoiceProcessor.h b/Source/VoiceProcessor.h
index 60b9792..3558ee9 100644
--- a/Source/VoiceProcessor.h
+++ b/Source/VoiceProcessor.h
@@ -7,34 +7,35 @@
class VoiceProcessor {
public:
- VoiceProcessor();
+ VoiceProcessor() = default;
VoiceProcessor(const VoiceProcessor& other);
~VoiceProcessor();
juce::MidiBuffer processBuffer(const juce::MidiBuffer& buffer, int channel, int lengthSamples, bool bypassed);
void reset();
- void updateConfiguration(Configuration* configuration);
+ void updateConfiguration(Configuration* c);
private:
Configuration* configuration;
int bufferSizeSamples;
unsigned long long readHeadPosition = 0;
+ int lastKeyswitch = -1;
std::vector bufferedNotes;
std::optional lastWrittenNote;
BufferedNote* heldNoteAtWritePosition = nullptr;
std::optional previousNoteAtWritePosition;
std::optional previousNoteAtReadPosition;
- int writePositionCCStates[128] = { 0 };
- int readPositionCCStates[128] = { 0 };
- int writePositionHeldNotes[128] = { 0 };
- int readPositionHeldNotes[128] = { 0 };
+ std::array writePositionCCStates;
+ std::array readPositionCCStates;
+ std::array writePositionHeldNotes;
+ std::array readPositionHeldNotes;
int readPositionProgram = 0;
int writePositionProgram = 0;
std::vector unprocessedBuffer;
- unsigned long long getReadPosition();
- unsigned long long getWritePosition();
+ unsigned long long getReadPosition() const;
+ unsigned long long getWritePosition() const;
std::vector processSample(const std::optional>& enteredMessages, int channel, bool bypassed);
};
\ No newline at end of file
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..589268e
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+1.3.0
\ No newline at end of file
diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt
index 9ede182..0679d40 100644
--- a/deps/CMakeLists.txt
+++ b/deps/CMakeLists.txt
@@ -1 +1 @@
-add_subdirectory(JUCE)
+add_subdirectory(juce)