Browse Source

Prepare v2.6.0 for library

pull/47/head
hemmer 1 year ago
parent
commit
f95bb66c6c
11 changed files with 3 additions and 11315 deletions
  1. +0
    -84
      .github/workflows/build-plugin.yml
  2. +3
    -3
      CHANGELOG.md
  3. +0
    -36
      plugin.json
  4. +0
    -1850
      res/panels/EvenVCObeta.svg
  5. +0
    -6828
      res/panels/MidiThing.svg
  6. +0
    -1147
      res/panels/PonyVCF.svg
  7. +0
    -331
      src/EvenVCO2.cpp
  8. +0
    -806
      src/MidiThing.cpp
  9. +0
    -224
      src/PonyVCF.cpp
  10. +0
    -3
      src/plugin.cpp
  11. +0
    -3
      src/plugin.hpp

+ 0
- 84
.github/workflows/build-plugin.yml View File

@@ -1,84 +0,0 @@
name: Build VCV Rack Plugin
on: [push, pull_request]

env:
rack-sdk-version: 2.4.1
rack-plugin-toolchain-dir: /home/build/rack-plugin-toolchain

defaults:
run:
shell: bash

jobs:
build:
name: ${{ matrix.platform }}
runs-on: ubuntu-latest
container:
image: ghcr.io/qno/rack-plugin-toolchain-win-linux
options: --user root
strategy:
fail-fast: false
matrix:
platform: [win-x64, lin-x64]
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Build plugin
run: |
export PLUGIN_DIR=$GITHUB_WORKSPACE
pushd ${{ env.rack-plugin-toolchain-dir }}
make plugin-build-${{ matrix.platform }}
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
path: ${{ env.rack-plugin-toolchain-dir }}/plugin-build
name: ${{ matrix.platform }}

build-mac:
name: mac
runs-on: macos-12
strategy:
fail-fast: false
matrix:
platform: [x64, arm64]
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Get Rack-SDK
run: |
pushd $HOME
curl -o Rack-SDK.zip https://vcvrack.com/downloads/Rack-SDK-${{ env.rack-sdk-version }}-mac-${{ matrix.platform }}.zip
unzip Rack-SDK.zip
- name: Build plugin
run: |
CROSS_COMPILE_TARGET_x64=x86_64-apple-darwin
CROSS_COMPILE_TARGET_arm64=arm64-apple-darwin
export RACK_DIR=$HOME/Rack-SDK
export CROSS_COMPILE=$CROSS_COMPILE_TARGET_${{ matrix.platform }}
make dep
make dist
echo "Plugin architecture '$(lipo -archs plugin.dylib)'"
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
path: dist/*.vcvplugin
name: mac-${{ matrix.platform }}

publish:
name: Publish plugin
runs-on: ubuntu-latest
needs: [build, build-mac]
steps:
- uses: actions/download-artifact@v3
with:
path: _artifacts
- uses: "marvinpinto/action-automatic-releases@latest"
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
automatic_release_tag: "latest"
prerelease: true
title: "Development Build"
files: |
_artifacts/**/*.vcvplugin

+ 3
- 3
CHANGELOG.md View File

@@ -1,11 +1,11 @@
# Change Log


## v2.6.0 (in progress)
* Midi Thing 2
* Initial release
## v2.6.0
* Octaves
* Initial release
* Misc
* Better default values for ADSR and Burst


## v2.5.0


+ 0
- 36
plugin.json View File

@@ -23,18 +23,6 @@
"Polyphonic"
]
},
{
"slug": "EvenVCO2",
"name": "Even VCO (beta)",
"description": "Oscillator including even-harmonic waveform",
"manualUrl": "https://www.befaco.org/even-vco/",
"modularGridUrl": "https://www.modulargrid.net/e/befaco-even-vco-",
"tags": [
"VCO",
"Hardware clone",
"Polyphonic"
]
},
{
"slug": "Rampage",
"name": "Rampage",
@@ -308,18 +296,6 @@
"Hardware clone"
]
},
{
"slug": "MidiThingV2",
"name": "MIDI Thing V2",
"description": "Hardware MIDI Thing v2 is a flexible MIDI to CV converter, this module acts as a bridge from VCV",
"manualUrl": "https://github.com/VCVRack/Befaco/blob/v2/docs/MIDIThingV2.md",
"modularGridUrl": "https://www.modulargrid.net/e/befaco-midi-thing-v2",
"tags": [
"External",
"MIDI",
"Hardware clone"
]
},
{
"slug": "Voltio",
"name": "Voltio",
@@ -342,18 +318,6 @@
"Hardware clone",
"VCO"
]
},
{
"slug": "PonyVCF",
"name": "PonyVCF",
"description": "Space-conscious lowpass filter and volume processor.",
"manualUrl": "https://www.befaco.org/pony-vcf/",
"modularGridUrl": "https://www.modulargrid.net/e/befaco-pony-vcf-",
"tags": [
"Hardware clone",
"Mixer",
"Filter"
]
}
]
}

+ 0
- 1850
res/panels/EvenVCObeta.svg
File diff suppressed because it is too large
View File


+ 0
- 6828
res/panels/MidiThing.svg
File diff suppressed because it is too large
View File


+ 0
- 1147
res/panels/PonyVCF.svg
File diff suppressed because it is too large
View File


+ 0
- 331
src/EvenVCO2.cpp View File

@@ -1,331 +0,0 @@
#include "plugin.hpp"
#include "ChowDSP.hpp"

using simd::float_4;

struct EvenVCO2 : Module {
enum ParamIds {
OCTAVE_PARAM,
TUNE_PARAM,
PWM_PARAM,
NUM_PARAMS
};
enum InputIds {
PITCH1_INPUT,
PITCH2_INPUT,
FM_INPUT,
SYNC_INPUT,
PWM_INPUT,
NUM_INPUTS
};
enum OutputIds {
TRI_OUTPUT,
SINE_OUTPUT,
EVEN_OUTPUT,
SAW_OUTPUT,
SQUARE_OUTPUT,
NUM_OUTPUTS
};


float_4 phase[4] = {};
dsp::TSchmittTrigger<float_4> syncTrigger[4];
bool removePulseDC = true;
bool limitPW = true;

EvenVCO2() {
config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS);
configParam(OCTAVE_PARAM, -5.0, 4.0, 0.0, "Octave", "'", 0.5);
getParamQuantity(OCTAVE_PARAM)->snapEnabled = true;
configParam(TUNE_PARAM, -7.0, 7.0, 0.0, "Tune", " semitones");
configParam(PWM_PARAM, -1.0, 1.0, 0.0, "Pulse width");

configInput(PITCH1_INPUT, "Pitch 1");
configInput(PITCH2_INPUT, "Pitch 2");
configInput(FM_INPUT, "FM");
configInput(SYNC_INPUT, "Sync");
configInput(PWM_INPUT, "Pulse Width Modulation");

configOutput(TRI_OUTPUT, "Triangle");
configOutput(SINE_OUTPUT, "Sine");
configOutput(EVEN_OUTPUT, "Even");
configOutput(SAW_OUTPUT, "Sawtooth");
configOutput(SQUARE_OUTPUT, "Square");

// calculate up/downsampling rates
onSampleRateChange();
}

void onSampleRateChange() override {
float sampleRate = APP->engine->getSampleRate();
for (int i = 0; i < NUM_OUTPUTS; ++i) {
for (int c = 0; c < 4; c++) {
oversampler[i][c].setOversamplingIndex(oversamplingIndex);
oversampler[i][c].reset(sampleRate);
}
}

const float lowFreqRegime = oversampler[0][0].getOversamplingRatio() * 1e-3 * sampleRate;
DEBUG("Low freq regime: %g", lowFreqRegime);
}

float_4 aliasSuppressedTri(float_4* phases) {
float_4 triBuffer[3];
for (int i = 0; i < 3; ++i) {
float_4 p = 2 * phases[i] - 1.0; // range -1.0 to +1.0
float_4 s = 0.5 - simd::abs(p); // eq 30
triBuffer[i] = (s * s * s - 0.75 * s) / 3.0; // eq 29
}
return (triBuffer[0] - 2.0 * triBuffer[1] + triBuffer[2]);
}

float_4 aliasSuppressedSaw(float_4* phases) {
float_4 sawBuffer[3];
for (int i = 0; i < 3; ++i) {
float_4 p = 2 * phases[i] - 1.0; // range -1 to +1
sawBuffer[i] = (p * p * p - p) / 6.0; // eq 11
}

return (sawBuffer[0] - 2.0 * sawBuffer[1] + sawBuffer[2]);
}

float_4 aliasSuppressedDoubleSaw(float_4* phases) {
float_4 sawBuffer[3];
for (int i = 0; i < 3; ++i) {
float_4 p = 4.0 * simd::ifelse(phases[i] < 0.5, phases[i], phases[i] - 0.5) - 1.0;
sawBuffer[i] = (p * p * p - p) / 24.0; // eq 11 (modified for doubled freq)
}

return (sawBuffer[0] - 2.0 * sawBuffer[1] + sawBuffer[2]);
}

float_4 aliasSuppressedOffsetSaw(float_4* phases, float_4 pw) {
float_4 sawOffsetBuff[3];

for (int i = 0; i < 3; ++i) {
float_4 p = 2 * phases[i] - 1.0; // range -1 to +1
float_4 pwp = p + 2 * pw; // phase after pw (pw in [0, 1])
pwp += simd::ifelse(pwp > 1, -2, 0); // modulo on [-1, +1]
sawOffsetBuff[i] = (pwp * pwp * pwp - pwp) / 6.0; // eq 11
}
return (sawOffsetBuff[0] - 2.0 * sawOffsetBuff[1] + sawOffsetBuff[2]);
}

chowdsp::VariableOversampling<6, float_4> oversampler[NUM_OUTPUTS][4]; // uses a 2*6=12th order Butterworth filter
int oversamplingIndex = 1; // default is 2^oversamplingIndex == x2 oversampling

void process(const ProcessArgs& args) override {

// pitch inputs determine number of polyphony engines
const int channels = std::max({1, inputs[PITCH1_INPUT].getChannels(), inputs[PITCH2_INPUT].getChannels()});

const float pitchKnobs = 1.f + std::round(params[OCTAVE_PARAM].getValue()) + params[TUNE_PARAM].getValue() / 12.f;
const int oversamplingRatio = oversampler[0][0].getOversamplingRatio();

for (int c = 0; c < channels; c += 4) {
float_4 pw = simd::clamp(params[PWM_PARAM].getValue() + inputs[PWM_INPUT].getPolyVoltageSimd<float_4>(c) / 5.f, -1.f, 1.f);
if (limitPW) {
pw = simd::rescale(pw, -1, +1, 0.05f, 0.95f);
}
else {
pw = simd::rescale(pw, -1.f, +1.f, 0.f, 1.f);
}

const float_4 fmVoltage = inputs[FM_INPUT].getPolyVoltageSimd<float_4>(c) * 0.25f;
const float_4 pitch = inputs[PITCH1_INPUT].getPolyVoltageSimd<float_4>(c) + inputs[PITCH2_INPUT].getPolyVoltageSimd<float_4>(c);
const float_4 freq = dsp::FREQ_C4 * simd::pow(2.f, pitchKnobs + pitch + fmVoltage);
const float_4 deltaBasePhase = simd::clamp(freq * args.sampleTime / oversamplingRatio, 1e-6, 0.5f);
// floating point arithmetic doesn't work well at low frequencies, specifically because the finite difference denominator
// becomes tiny - we check for that scenario and use naive / 1st order waveforms in that frequency regime (as aliasing isn't
// a problem there). With no oversampling, at 44100Hz, the threshold frequency is 44.1Hz.
const float_4 lowFreqRegime = simd::abs(deltaBasePhase) < 1e-3;
// 1 / denominator for the second-order FD
const float_4 denominatorInv = 0.25 / (deltaBasePhase * deltaBasePhase);

// pulsewave waveform doesn't have DC even for non 50% duty cycles, but Befaco team would like the option
// for it to be added back in for hardware compatibility reasons
const float_4 pulseDCOffset = (!removePulseDC) * 2.f * (0.5f - pw);

// hard sync
const float_4 syncMask = syncTrigger[c / 4].process(inputs[SYNC_INPUT].getPolyVoltageSimd<float_4>(c));
phase[c / 4] = simd::ifelse(syncMask, 0.5f, phase[c / 4]);

float_4* osBufferTri = oversampler[TRI_OUTPUT][c / 4].getOSBuffer();
float_4* osBufferSaw = oversampler[SAW_OUTPUT][c / 4].getOSBuffer();
float_4* osBufferSin = oversampler[SINE_OUTPUT][c / 4].getOSBuffer();
float_4* osBufferSquare = oversampler[SQUARE_OUTPUT][c / 4].getOSBuffer();
float_4* osBufferEven = oversampler[EVEN_OUTPUT][c / 4].getOSBuffer();
for (int i = 0; i < oversamplingRatio; ++i) {

phase[c / 4] += deltaBasePhase;
// ensure within [0, 1]
phase[c / 4] -= simd::floor(phase[c / 4]);

float_4 phases[3]; // phase as extrapolated to the current and two previous samples

phases[0] = phase[c / 4] - 2 * deltaBasePhase + simd::ifelse(phase[c / 4] < 2 * deltaBasePhase, 1.f, 0.f);
phases[1] = phase[c / 4] - deltaBasePhase + simd::ifelse(phase[c / 4] < deltaBasePhase, 1.f, 0.f);
phases[2] = phase[c / 4];

if (outputs[SINE_OUTPUT].isConnected() || outputs[EVEN_OUTPUT].isConnected()) {
// sin doesn't need PDW
osBufferSin[i] = -simd::cos(2.0 * M_PI * phase[c / 4]);
}

if (outputs[TRI_OUTPUT].isConnected()) {
const float_4 dpwOrder1 = 1.0 - 2.0 * simd::abs(2 * phase[c / 4] - 1.0);
const float_4 dpwOrder3 = aliasSuppressedTri(phases) * denominatorInv;

osBufferTri[i] = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3);
}

if (outputs[SAW_OUTPUT].isConnected()) {
const float_4 dpwOrder1 = 2 * phase[c / 4] - 1.0;
const float_4 dpwOrder3 = aliasSuppressedSaw(phases) * denominatorInv;

osBufferSaw[i] = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3);
}

if (outputs[SQUARE_OUTPUT].isConnected()) {

float_4 dpwOrder1 = simd::ifelse(phase[c / 4] < pw, -1.0, +1.0);
dpwOrder1 -= removePulseDC ? 2.f * (0.5f - pw) : 0.f;

float_4 saw = aliasSuppressedSaw(phases);
float_4 sawOffset = aliasSuppressedOffsetSaw(phases, pw);
float_4 dpwOrder3 = (saw - sawOffset) * denominatorInv + pulseDCOffset;

osBufferSquare[i] = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3);
}

if (outputs[EVEN_OUTPUT].isConnected()) {

float_4 dpwOrder1 = 4.0 * simd::ifelse(phase[c / 4] < 0.5, phase[c / 4], phase[c / 4] - 0.5) - 1.0;
float_4 dpwOrder3 = aliasSuppressedDoubleSaw(phases) * denominatorInv;
float_4 doubleSaw = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3);
osBufferEven[i] = 0.55 * (doubleSaw + 1.27 * osBufferSin[i]);
}


} // end of oversampling loop

// downsample (if required)
if (outputs[SINE_OUTPUT].isConnected()) {
const float_4 outSin = (oversamplingRatio > 1) ? oversampler[SINE_OUTPUT][c / 4].downsample() : osBufferSin[0];
outputs[SINE_OUTPUT].setVoltageSimd(5.f * outSin, c);
}

if (outputs[TRI_OUTPUT].isConnected()) {
const float_4 outTri = (oversamplingRatio > 1) ? oversampler[TRI_OUTPUT][c / 4].downsample() : osBufferTri[0];
outputs[TRI_OUTPUT].setVoltageSimd(5.f * outTri, c);
}

if (outputs[SAW_OUTPUT].isConnected()) {
const float_4 outSaw = (oversamplingRatio > 1) ? oversampler[SAW_OUTPUT][c / 4].downsample() : osBufferSaw[0];
outputs[SAW_OUTPUT].setVoltageSimd(5.f * outSaw, c);
}

if (outputs[SQUARE_OUTPUT].isConnected()) {
const float_4 outSquare = (oversamplingRatio > 1) ? oversampler[SQUARE_OUTPUT][c / 4].downsample() : osBufferSquare[0];
outputs[SQUARE_OUTPUT].setVoltageSimd(5.f * outSquare, c);
}

if (outputs[EVEN_OUTPUT].isConnected()) {
const float_4 outEven = (oversamplingRatio > 1) ? oversampler[EVEN_OUTPUT][c / 4].downsample() : osBufferEven[0];
outputs[EVEN_OUTPUT].setVoltageSimd(5.f * outEven, c);
}

} // end of channels loop

// Outputs
outputs[TRI_OUTPUT].setChannels(channels);
outputs[SINE_OUTPUT].setChannels(channels);
outputs[EVEN_OUTPUT].setChannels(channels);
outputs[SAW_OUTPUT].setChannels(channels);
outputs[SQUARE_OUTPUT].setChannels(channels);
}


json_t* dataToJson() override {
json_t* rootJ = json_object();
json_object_set_new(rootJ, "removePulseDC", json_boolean(removePulseDC));
json_object_set_new(rootJ, "limitPW", json_boolean(limitPW));
json_object_set_new(rootJ, "oversamplingIndex", json_integer(oversampler[0][0].getOversamplingIndex()));
return rootJ;
}

void dataFromJson(json_t* rootJ) override {
json_t* pulseDCJ = json_object_get(rootJ, "removePulseDC");
if (pulseDCJ) {
removePulseDC = json_boolean_value(pulseDCJ);
}

json_t* limitPWJ = json_object_get(rootJ, "limitPW");
if (limitPWJ) {
limitPW = json_boolean_value(limitPWJ);
}

json_t* oversamplingIndexJ = json_object_get(rootJ, "oversamplingIndex");
if (oversamplingIndexJ) {
oversamplingIndex = json_integer_value(oversamplingIndexJ);
onSampleRateChange();
}
}
};


struct EvenVCO2Widget : ModuleWidget {
EvenVCO2Widget(EvenVCO2* module) {
setModule(module);
setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/panels/EvenVCObeta.svg")));

addChild(createWidget<Knurlie>(Vec(15, 0)));
addChild(createWidget<Knurlie>(Vec(15, 365)));
addChild(createWidget<Knurlie>(Vec(15 * 6, 0)));
addChild(createWidget<Knurlie>(Vec(15 * 6, 365)));

addParam(createParam<BefacoBigKnob>(Vec(22, 32), module, EvenVCO2::OCTAVE_PARAM));
addParam(createParam<BefacoTinyKnob>(Vec(73, 131), module, EvenVCO2::TUNE_PARAM));
addParam(createParam<Davies1900hRedKnob>(Vec(16, 230), module, EvenVCO2::PWM_PARAM));

addInput(createInput<BefacoInputPort>(Vec(8, 120), module, EvenVCO2::PITCH1_INPUT));
addInput(createInput<BefacoInputPort>(Vec(19, 157), module, EvenVCO2::PITCH2_INPUT));
addInput(createInput<BefacoInputPort>(Vec(48, 183), module, EvenVCO2::FM_INPUT));
addInput(createInput<BefacoInputPort>(Vec(86, 189), module, EvenVCO2::SYNC_INPUT));

addInput(createInput<BefacoInputPort>(Vec(72, 236), module, EvenVCO2::PWM_INPUT));

addOutput(createOutput<BefacoOutputPort>(Vec(10, 283), module, EvenVCO2::TRI_OUTPUT));
addOutput(createOutput<BefacoOutputPort>(Vec(87, 283), module, EvenVCO2::SINE_OUTPUT));
addOutput(createOutput<BefacoOutputPort>(Vec(48, 306), module, EvenVCO2::EVEN_OUTPUT));
addOutput(createOutput<BefacoOutputPort>(Vec(10, 327), module, EvenVCO2::SAW_OUTPUT));
addOutput(createOutput<BefacoOutputPort>(Vec(87, 327), module, EvenVCO2::SQUARE_OUTPUT));
}

void appendContextMenu(Menu* menu) override {
EvenVCO2* module = dynamic_cast<EvenVCO2*>(this->module);
assert(module);

menu->addChild(new MenuSeparator());
menu->addChild(createSubmenuItem("Hardware compatibility", "",
[ = ](Menu * menu) {
menu->addChild(createBoolPtrMenuItem("Remove DC from pulse", "", &module->removePulseDC));
menu->addChild(createBoolPtrMenuItem("Limit pulsewidth (5\%-95\%)", "", &module->limitPW));
}
));

menu->addChild(createIndexSubmenuItem("Oversampling",
{"Off", "x2", "x4", "x8"},
[ = ]() {
return module->oversamplingIndex;
},
[ = ](int mode) {
module->oversamplingIndex = mode;
module->onSampleRateChange();
}
));
}
};


Model* modelEvenVCO2 = createModel<EvenVCO2, EvenVCO2Widget>("EvenVCO2");

+ 0
- 806
src/MidiThing.cpp View File

@@ -1,806 +0,0 @@
#include "plugin.hpp"


/*! \brief Decode System Exclusive messages.
SysEx messages are encoded to guarantee transmission of data bytes higher than
127 without breaking the MIDI protocol. Use this static method to reassemble
your received message.
\param inSysEx The SysEx data received from MIDI in.
\param outData The output buffer where to store the decrypted message.
\param inLength The length of the input buffer.
\param inFlipHeaderBits True for Korg and other who store MSB in reverse order
\return The length of the output buffer.
@see encodeSysEx @see getSysExArrayLength
Code inspired from Ruin & Wesen's SysEx encoder/decoder - http://ruinwesen.com
*/
unsigned decodeSysEx(const uint8_t* inSysEx,
uint8_t* outData,
unsigned inLength,
bool inFlipHeaderBits) {
unsigned count = 0;
uint8_t msbStorage = 0;
uint8_t byteIndex = 0;

for (unsigned i = 0; i < inLength; ++i) {
if ((i % 8) == 0) {
msbStorage = inSysEx[i];
byteIndex = 6;
}
else {
const uint8_t body = inSysEx[i];
const uint8_t shift = inFlipHeaderBits ? 6 - byteIndex : byteIndex;
const uint8_t msb = uint8_t(((msbStorage >> shift) & 1) << 7);
byteIndex--;
outData[count++] = msb | body;
}
}
return count;
}

struct RoundRobinProcessor {
// if a channel (0 - 11) should be updated, return it's index, otherwise return -1
int process(float sampleTime, float period, int numActiveChannels) {

if (numActiveChannels == 0 || period <= 0) {
return -1;
}

time += sampleTime;

if (time > period) {
time -= period;

// special case: when there's only one channel, the below logic (which looks for when active channel changes)
// wont fire. as we've completed a period, return an "update channel 0" value
if (numActiveChannels == 1) {
return 0;
}
}

int currentActiveChannel = numActiveChannels * time / period;

if (currentActiveChannel != previousActiveChannel) {
previousActiveChannel = currentActiveChannel;
return currentActiveChannel;
}

// if we've got this far, no updates needed (-1)
return -1;
}
private:
float time = 0.f;
int previousActiveChannel = -1;
};


struct MidiThing : Module {
enum ParamId {
REFRESH_PARAM,
PARAMS_LEN
};
enum InputId {
A1_INPUT,
B1_INPUT,
C1_INPUT,
A2_INPUT,
B2_INPUT,
C2_INPUT,
A3_INPUT,
B3_INPUT,
C3_INPUT,
A4_INPUT,
B4_INPUT,
C4_INPUT,
INPUTS_LEN
};
enum OutputId {
OUTPUTS_LEN
};
enum LightId {
LIGHTS_LEN
};
/// Port mode
enum PORTMODE_t {
NOPORTMODE = 0,
MODE10V,
MODEPN5V,
MODENEG10V,
MODE8V,
MODE5V,

LASTPORTMODE
};

const char* cfgPortModeNames[7] = {
"No Mode",
"0/10v",
"-5/5v",
"-10/0v",
"0/8v",
"0/5v",
""
};

const std::vector<float> updateRates = {250., 500., 1000., 2000., 4000., 8000.};
const std::vector<std::string> updateRateNames = {"250 Hz (fewest active channels, slowest, lowest-cpu)", "500 Hz", "1 kHz", "2 kHz", "4 kHz",
"8 kHz (most active channels, fast, highest-cpu)"
};
int updateRateIdx = 2;

// use Pre-def 4 for bridge mode
const static int VCV_BRIDGE_PREDEF = 4;

midi::Output midiOut;
RoundRobinProcessor roundRobinProcessor;

MidiThing() {
config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN);
configButton(REFRESH_PARAM, "");

for (int i = 0; i < NUM_INPUTS; ++i) {
portModes[i] = MODE10V;
configInput(A1_INPUT + i, string::f("Port %d", i + 1));
}
}

void onReset() override {
midiOut.reset();

}

void requestAllChannelsParamsOverSysex() {
for (int row = 0; row < 4; ++row) {
for (int col = 0; col < 3; ++col) {
const int PORT_CONFIG = 2;
requestParamOverSysex(row, col, PORT_CONFIG);
}
}
}

// request that MidiThing loads a pre-defined template, 1-4
void setPredef(uint8_t predef) {
predef = clamp(predef, 1, 4);
midi::Message msg;
msg.bytes.resize(8);
// Midi spec is zeroo indexed
uint8_t predefToSend = predef - 1;
msg.bytes = {0xF0, 0x7D, 0x17, 0x00, 0x00, 0x02, 0x00, predefToSend, 0xF7};
midiOut.setChannel(0);
midiOut.sendMessage(msg);
// DEBUG("Predef %d msg request sent: %s", predef, msg.toString().c_str());
}

void setMidiMergeViaSysEx(bool mergeOn) {
midi::Message msg;
msg.bytes.resize(8);

msg.bytes = {0xF0, 0x7D, 0x19, 0x00, 0x05, 0x02, 0x00, (uint8_t) mergeOn, 0xF7};
midiOut.setChannel(0);
midiOut.sendMessage(msg);
// DEBUG("Predef %d msg request sent: %s", mergeOn, msg.toString().c_str());
}


void setVoltageModeOnHardware(uint8_t row, uint8_t col, PORTMODE_t outputMode_) {
uint8_t port = 3 * row + col;
portModes[port] = outputMode_;

midi::Message msg;
msg.bytes.resize(8);
// F0 7D 17 2n 02 02 00 0m F7
// Where n = 0 based port number
// and m is the volt output mode to select from:
msg.bytes = {0xF0, 0x7D, 0x17, static_cast<unsigned char>(32 + port), 0x02, 0x02, 0x00, (uint8_t) portModes[port], 0xF7};
midiOut.sendMessage(msg);
// DEBUG("Voltage mode msg sent: port %d (%d), mode %d", port, static_cast<unsigned char>(32 + port), portModes[port]);
}

void setVoltageModeOnHardware(uint8_t row, uint8_t col) {
setVoltageModeOnHardware(row, col, portModes[3 * row + col]);
}

void syncVcvStateToHardware() {
for (int row = 0; row < 4; ++row) {
for (int col = 0; col < 3; ++col) {
setVoltageModeOnHardware(row, col);
}
}
}


midi::InputQueue inputQueue;
void requestParamOverSysex(uint8_t row, uint8_t col, uint8_t mode) {

midi::Message msg;
msg.bytes.resize(8);
// F0 7D 17 00 01 03 00 nm pp F7
uint8_t port = 3 * row + col;
//Where n is:
// 0 = Full configuration request. The module will send only pre def, port functions and modified parameters
// 2 = Send Port configuration
// 4 = Send MIDI Channel configuration
// 6 = Send Voice Configuration

uint8_t n = mode * 16;
uint8_t m = port; // element number: 0-11 port number, 1-16 channel or voice number
uint8_t pp = 2;
msg.bytes = {0xF0, 0x7D, 0x17, 0x00, 0x01, 0x03, 0x00, static_cast<uint8_t>(n + m), pp, 0xF7};
midiOut.sendMessage(msg);
// DEBUG("API request mode msg sent: port %d, pp %s", port, msg.toString().c_str());
}

int getVoltageMode(uint8_t row, uint8_t col) {
// -1 because menu is zero indexed but enum is not
int channel = clamp(3 * row + col, 0, NUM_INPUTS - 1);
return portModes[channel] - 1;
}

const static int NUM_INPUTS = 12;
bool isClipping[NUM_INPUTS] = {};

bool checkIsVoltageWithinRange(uint8_t channel, float voltage) {
const float tol = 0.001;
switch (portModes[channel]) {
case MODE10V: return 0 - tol < voltage && voltage < 10 + tol;
case MODEPN5V: return -5 - tol < voltage && voltage < 5 + tol;
case MODENEG10V: return -10 - tol < voltage && voltage < 0 + tol;
case MODE8V: return 0 - tol < voltage && voltage < 8 + tol;
case MODE5V: return 0 - tol < voltage && voltage < 5 + tol;
default: return false;
}
}

uint16_t rescaleVoltageForChannel(uint8_t channel, float voltage) {
switch (portModes[channel]) {
case MODE10V: return rescale(clamp(voltage, 0.f, 10.f), 0.f, +10.f, 0, 16383);
case MODEPN5V: return rescale(clamp(voltage, -5.f, 5.f), -5.f, +5.f, 0, 16383);
case MODENEG10V: return rescale(clamp(voltage, -10.f, 0.f), -10.f, +0.f, 0, 16383);
case MODE8V: return rescale(clamp(voltage, 0.f, 8.f), 0.f, +8.f, 0, 16383);
case MODE5V: return rescale(clamp(voltage, 0.f, 5.f), 0.f, +5.f, 0, 16383);
default: return 0;
}
}

// one way sync (VCV -> hardware) for now
void doSync() {
// switch to VCV template (predef 4)
setPredef(4);

// disable MIDI merge (otherwise large sample rates will not work)
setMidiMergeViaSysEx(false);

// send full VCV config
syncVcvStateToHardware();

// disabled for now, but this would request what state the hardware is in
if (parseSysExMessagesFromHardware) {
requestAllChannelsParamsOverSysex();
}
}

// debug only
bool parseSysExMessagesFromHardware = false;
int numActiveChannels = 0;
dsp::BooleanTrigger buttonTrigger;
dsp::Timer rateLimiterTimer;
PORTMODE_t portModes[NUM_INPUTS] = {};
void process(const ProcessArgs& args) override {

if (buttonTrigger.process(params[REFRESH_PARAM].getValue())) {
doSync();
}

// disabled for now, but this is how VCV would read SysEx coming from the hardware (if requested above)
if (parseSysExMessagesFromHardware) {
midi::Message msg;
uint8_t outData[32] = {};
while (inputQueue.tryPop(&msg, args.frame)) {
// DEBUG("msg (size: %d): %s", msg.getSize(), msg.toString().c_str());

uint8_t outLen = decodeSysEx(&msg.bytes[0], outData, msg.bytes.size(), false);
if (outLen > 3) {

int channel = (outData[2] & 0x0f) >> 0;

if (channel >= 0 && channel < NUM_INPUTS) {
if (outData[outLen - 1] < LASTPORTMODE) {
portModes[channel] = (PORTMODE_t) outData[outLen - 1];
// DEBUG("Channel %d, %d: mode %d (%s)", outData[2], channel, portModes[channel], cfgPortModeNames[portModes[channel]]);
}
}
}
}
}

std::vector<int> activeChannels;
for (int c = 0; c < NUM_INPUTS; ++c) {
if (inputs[A1_INPUT + c].isConnected()) {
activeChannels.push_back(c);
}
}
numActiveChannels = activeChannels.size();
// we're done if no channels are active
if (numActiveChannels == 0) {
return;
}

//DEBUG("updateRateIdx: %d", updateRateIdx);
const float updateRateHz = updateRates[updateRateIdx];
//DEBUG("updateRateHz: %f", updateRateHz);
const int maxCCMessagesPerSecondPerChannel = updateRateHz / numActiveChannels;

// MIDI baud rate is 31250 b/s, or 3125 B/s.
// CC messages are 3 bytes, so we can send a maximum of 1041 CC messages per second.
// The refresh rate period (i.e. how often we can send X channels of data is:
const float rateLimiterPeriod = 1.f / maxCCMessagesPerSecondPerChannel;

// this returns -1 if no channel should be updated, or the index of the channel that should be updated
// it distributes update times in a round robin fashion
int channelIdxToUpdate = roundRobinProcessor.process(args.sampleTime, rateLimiterPeriod, numActiveChannels);

if (channelIdxToUpdate >= 0 && channelIdxToUpdate < numActiveChannels) {
int c = activeChannels[channelIdxToUpdate];

const float channelVoltage = inputs[A1_INPUT + c].getVoltage();
uint16_t pw = rescaleVoltageForChannel(c, channelVoltage);
isClipping[c] = !checkIsVoltageWithinRange(c, channelVoltage);
midi::Message m;
m.setStatus(0xe);
m.setNote(pw & 0x7f);
m.setValue((pw >> 7) & 0x7f);
m.setFrame(args.frame);

midiOut.setChannel(c);
midiOut.sendMessage(m);
}
}


json_t* dataToJson() override {
json_t* rootJ = json_object();
json_object_set_new(rootJ, "midiOutput", midiOut.toJson());
json_object_set_new(rootJ, "inputQueue", inputQueue.toJson());
json_object_set_new(rootJ, "updateRateIdx", json_integer(updateRateIdx));

for (int c = 0; c < NUM_INPUTS; ++c) {
json_object_set_new(rootJ, string::f("portMode%d", c).c_str(), json_integer(portModes[c]));
}

return rootJ;
}

void dataFromJson(json_t* rootJ) override {
json_t* midiOutputJ = json_object_get(rootJ, "midiOutput");
if (midiOutputJ) {
midiOut.fromJson(midiOutputJ);
}

json_t* midiInputQueueJ = json_object_get(rootJ, "inputQueue");
if (midiInputQueueJ) {
inputQueue.fromJson(midiInputQueueJ);
}

json_t* updateRateIdxJ = json_object_get(rootJ, "updateRateIdx");
if (updateRateIdxJ) {
updateRateIdx = json_integer_value(updateRateIdxJ);
}

for (int c = 0; c < NUM_INPUTS; ++c) {
json_t* portModeJ = json_object_get(rootJ, string::f("portMode%d", c).c_str());
if (portModeJ) {
portModes[c] = (PORTMODE_t)json_integer_value(portModeJ);
}
}

// requestAllChannelsParamsOverSysex();
syncVcvStateToHardware();
}
};

struct MidiThingPort : PJ301MPort {
int row = 0, col = 0;
MidiThing* module;

void appendContextMenu(Menu* menu) override {

menu->addChild(new MenuSeparator());
std::string label = string::f("Voltage Mode Port %d", 3 * row + col + 1);

menu->addChild(createIndexSubmenuItem(label,
{"0 to 10v", "-5 to 5v", "-10 to 0v", "0 to 8v", "0 to 5v"},
[ = ]() {
return module->getVoltageMode(row, col);
},
[ = ](int modeIdx) {
MidiThing::PORTMODE_t mode = (MidiThing::PORTMODE_t)(modeIdx + 1);
module->setVoltageModeOnHardware(row, col, mode);
}
));

/*
menu->addChild(createIndexSubmenuItem("Get Port Info",
{"Full", "Port", "MIDI", "Voice"},
[ = ]() {
return -1;
},
[ = ](int mode) {
module->requestParamOverSysex(row, col, 2 * mode);
}
));
*/
}
};

// dervied from https://github.com/countmodula/VCVRackPlugins/blob/v2.0.0/src/components/CountModulaLEDDisplay.hpp
struct LEDDisplay : LightWidget {
float fontSize = 9;
Vec textPos = Vec(1, 13);
int numChars = 7;
int row = 0, col = 0;
MidiThing* module;

LEDDisplay() {
box.size = mm2px(Vec(9.298, 5.116));
}

void setCentredPos(Vec pos) {
box.pos.x = pos.x - box.size.x / 2;
box.pos.y = pos.y - box.size.y / 2;
}

void drawBackground(const DrawArgs& args) override {
// Background
NVGcolor backgroundColor = nvgRGB(0x20, 0x20, 0x20);
NVGcolor borderColor = nvgRGB(0x10, 0x10, 0x10);
nvgBeginPath(args.vg);
nvgRoundedRect(args.vg, 0.0, 0.0, box.size.x, box.size.y, 2.0);
nvgFillColor(args.vg, backgroundColor);
nvgFill(args.vg);
nvgStrokeWidth(args.vg, 1.0);
nvgStrokeColor(args.vg, borderColor);
nvgStroke(args.vg);
}

void drawLight(const DrawArgs& args) override {
// Background
NVGcolor backgroundColor = nvgRGB(0x20, 0x20, 0x20);
NVGcolor borderColor = nvgRGB(0x10, 0x10, 0x10);
NVGcolor textColor = nvgRGB(0xff, 0x10, 0x10);

nvgBeginPath(args.vg);
nvgRoundedRect(args.vg, 0.0, 0.0, box.size.x, box.size.y, 2.0);
nvgFillColor(args.vg, backgroundColor);
nvgFill(args.vg);
nvgStrokeWidth(args.vg, 1.0);

if (module) {
const bool isClipping = module->isClipping[col + row * 3];
if (isClipping) {
borderColor = nvgRGB(0xff, 0x20, 0x20);
}
}

nvgStrokeColor(args.vg, borderColor);
nvgStroke(args.vg);

std::shared_ptr<Font> font = APP->window->loadFont(asset::plugin(pluginInstance, "res/fonts/miso.otf"));

if (font && font->handle >= 0) {

std::string text = "?-?v"; // fallback if module not yet defined
if (module) {
text = module->cfgPortModeNames[module->getVoltageMode(row, col) + 1];
}
char buffer[numChars + 1];
int l = text.size();
if (l > numChars)
l = numChars;

nvgGlobalTint(args.vg, color::WHITE);

text.copy(buffer, l);
buffer[l] = '\0';

nvgFontSize(args.vg, fontSize);
nvgFontFaceId(args.vg, font->handle);
nvgFillColor(args.vg, textColor);
nvgTextAlign(args.vg, NVG_ALIGN_CENTER | NVG_ALIGN_BOTTOM);
NVGtextRow textRow;
nvgTextBreakLines(args.vg, text.c_str(), NULL, box.size.x, &textRow, 1);
nvgTextBox(args.vg, textPos.x, textPos.y, box.size.x, textRow.start, textRow.end);
}
}

void onButton(const ButtonEvent& e) override {
if (e.button == GLFW_MOUSE_BUTTON_RIGHT && e.action == GLFW_PRESS) {
ui::Menu* menu = createMenu();

menu->addChild(createMenuLabel(string::f("Voltage mode port %d:", col + 3 * row + 1)));

const std::string labels[5] = {"0 to 10v", "-5 to 5v", "-10 to 0v", "0 to 8v", "0 to 5v"};

for (int i = 0; i < 5; ++i) {
menu->addChild(createCheckMenuItem(labels[i], "",
[ = ]() {
return module->getVoltageMode(row, col) == i;
},
[ = ]() {
MidiThing::PORTMODE_t mode = (MidiThing::PORTMODE_t)(i + 1);
module->setVoltageModeOnHardware(row, col, mode);
}
));
}

e.consume(this);
return;
}

LightWidget::onButton(e);
}

};


struct MidiThingWidget : ModuleWidget {

struct LedDisplayCenterChoiceEx : LedDisplayChoice {
LedDisplayCenterChoiceEx() {
box.size = mm2px(math::Vec(0, 8.0));
color = nvgRGB(0xf0, 0xf0, 0xf0);
bgColor = nvgRGBAf(0, 0, 0, 0);
textOffset = math::Vec(0, 16);
}

void drawLayer(const DrawArgs& args, int layer) override {
nvgScissor(args.vg, RECT_ARGS(args.clipBox));
if (layer == 1) {
if (bgColor.a > 0.0) {
nvgBeginPath(args.vg);
nvgRect(args.vg, 0, 0, box.size.x, box.size.y);
nvgFillColor(args.vg, bgColor);
nvgFill(args.vg);
}

std::shared_ptr<window::Font> font = APP->window->loadFont(asset::plugin(pluginInstance, "res/fonts/miso.otf"));

if (font && font->handle >= 0 && !text.empty()) {
nvgFillColor(args.vg, color);
nvgFontFaceId(args.vg, font->handle);
nvgTextLetterSpacing(args.vg, -0.6f);
nvgFontSize(args.vg, 10);
nvgTextAlign(args.vg, NVG_ALIGN_CENTER | NVG_ALIGN_BOTTOM);
NVGtextRow textRow;
nvgTextBreakLines(args.vg, text.c_str(), NULL, box.size.x, &textRow, 1);
nvgTextBox(args.vg, textOffset.x, textOffset.y, box.size.x, textRow.start, textRow.end);
}
}
nvgResetScissor(args.vg);
}
};


struct MidiDriverItem : ui::MenuItem {
midi::Port* port;
int driverId;
void onAction(const event::Action& e) override {
port->setDriverId(driverId);
}
};

struct MidiDriverChoice : LedDisplayCenterChoiceEx {
midi::Port* port;
void onAction(const event::Action& e) override {
if (!port)
return;
createContextMenu();
}

virtual ui::Menu* createContextMenu() {
ui::Menu* menu = createMenu();
menu->addChild(createMenuLabel("MIDI driver"));
for (int driverId : midi::getDriverIds()) {
MidiDriverItem* item = new MidiDriverItem;
item->port = port;
item->driverId = driverId;
item->text = midi::getDriver(driverId)->getName();
item->rightText = CHECKMARK(item->driverId == port->driverId);
menu->addChild(item);
}
return menu;
}

void step() override {
text = port ? port->getDriver()->getName() : "";
if (text.empty()) {
text = "(No driver)";
color.a = 0.5f;
}
else {
color.a = 1.f;
}
}
};

struct MidiDeviceItem : ui::MenuItem {
midi::Port* outPort, *inPort;
int deviceId;
void onAction(const event::Action& e) override {
outPort->setDeviceId(deviceId);
inPort->setDeviceId(deviceId);
}
};

struct MidiDeviceChoice : LedDisplayCenterChoiceEx {
midi::Port* outPort, *inPort;
void onAction(const event::Action& e) override {
if (!outPort || !inPort)
return;
createContextMenu();
}

virtual ui::Menu* createContextMenu() {
ui::Menu* menu = createMenu();
menu->addChild(createMenuLabel("MIDI device"));
{
MidiDeviceItem* item = new MidiDeviceItem;
item->outPort = outPort;
item->inPort = inPort;
item->deviceId = -1;
item->text = "(No device)";
item->rightText = CHECKMARK(item->deviceId == outPort->deviceId);
menu->addChild(item);
}
for (int deviceId : outPort->getDeviceIds()) {
MidiDeviceItem* item = new MidiDeviceItem;
item->outPort = outPort;
item->inPort = inPort;
item->deviceId = deviceId;
item->text = outPort->getDeviceName(deviceId);
item->rightText = CHECKMARK(item->deviceId == outPort->deviceId);
menu->addChild(item);
}
return menu;
}

void step() override {
text = outPort ? outPort->getDeviceName(outPort->deviceId) : "";
if (text.empty()) {
text = "(No device)";
color.a = 0.5f;
}
else {
color.a = 1.f;
}
}
};

struct MidiWidget : LedDisplay {
MidiDriverChoice* driverChoice;
LedDisplaySeparator* driverSeparator;
MidiDeviceChoice* deviceChoice;
LedDisplaySeparator* deviceSeparator;

void setMidiPorts(midi::Port* outPort, midi::Port* inPort) {

clearChildren();
math::Vec pos;

MidiDriverChoice* driverChoice = createWidget<MidiDriverChoice>(pos);
driverChoice->box.size = Vec(box.size.x, 20.f);
//driverChoice->textOffset = Vec(6.f, 14.7f);
driverChoice->color = nvgRGB(0xf0, 0xf0, 0xf0);
driverChoice->port = outPort;

addChild(driverChoice);
pos = driverChoice->box.getBottomLeft();
this->driverChoice = driverChoice;

this->driverSeparator = createWidget<LedDisplaySeparator>(pos);
this->driverSeparator->box.size.x = box.size.x;
addChild(this->driverSeparator);

MidiDeviceChoice* deviceChoice = createWidget<MidiDeviceChoice>(pos);
deviceChoice->box.size = Vec(box.size.x, 21.f);
//deviceChoice->textOffset = Vec(6.f, 14.7f);
deviceChoice->color = nvgRGB(0xf0, 0xf0, 0xf0);
deviceChoice->outPort = outPort;
deviceChoice->inPort = inPort;
addChild(deviceChoice);
pos = deviceChoice->box.getBottomLeft();
this->deviceChoice = deviceChoice;
}
};


MidiThingWidget(MidiThing* module) {
setModule(module);
setPanel(createPanel(asset::plugin(pluginInstance, "res/panels/MidiThing.svg")));

addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, 0)));
addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));

MidiWidget* midiInputWidget = createWidget<MidiWidget>(Vec(1.5f, 36.4f)); //mm2px(Vec(0.5f, 10.f)));
midiInputWidget->box.size = mm2px(Vec(5.08 * 6 - 1, 13.5f));
if (module) {
midiInputWidget->setMidiPorts(&module->midiOut, &module->inputQueue);
}
else {
midiInputWidget->setMidiPorts(nullptr, nullptr);
}
addChild(midiInputWidget);

addParam(createParamCentered<BefacoButton>(mm2px(Vec(21.12, 57.32)), module, MidiThing::REFRESH_PARAM));

const float xStartLed = 0.2 + 0.628;
const float yStartLed = 28.019;

for (int row = 0; row < 4; row++) {
for (int col = 0; col < 3; col++) {

LEDDisplay* display = createWidget<LEDDisplay>(mm2px(Vec(xStartLed + 9.751 * col, yStartLed + 5.796 * row)));
display->module = module;
display->row = row;
display->col = col;
addChild(display);

auto input = createInputCentered<MidiThingPort>(mm2px(Vec(5.08 + 10 * col, 69.77 + 14.225 * row)), module, MidiThing::A1_INPUT + 3 * row + col);
input->row = row;
input->col = col;
input->module = module;
addInput(input);


}
}
}

void appendContextMenu(Menu* menu) override {
MidiThing* module = dynamic_cast<MidiThing*>(this->module);
assert(module);

menu->addChild(new MenuSeparator());

menu->addChild(createSubmenuItem("Select MIDI Device", "",
[ = ](Menu * menu) {

for (auto driverId : rack::midi::getDriverIds()) {
midi::Driver* driver = midi::getDriver(driverId);
const bool activeDriver = module->midiOut.getDriverId() == driverId;

menu->addChild(createSubmenuItem(driver->getName(), CHECKMARK(activeDriver),
[ = ](Menu * menu) {

for (auto deviceId : driver->getOutputDeviceIds()) {
const bool activeDevice = activeDriver && module->midiOut.getDeviceId() == deviceId;

menu->addChild(createMenuItem(driver->getOutputDeviceName(deviceId),
CHECKMARK(activeDevice),
[ = ]() {
module->midiOut.setDriverId(driverId);
module->midiOut.setDeviceId(deviceId);

module->inputQueue.setDriverId(driverId);
module->inputQueue.setDeviceId(deviceId);
module->inputQueue.setChannel(0); // TODO update

module->doSync();

// DEBUG("Updating Output MIDI settings - driver: %s, device: %s",
// driver->getName().c_str(), driver->getOutputDeviceName(deviceId).c_str());
}));
}
}));
}
}));

menu->addChild(createIndexPtrSubmenuItem("All channels MIDI update rate",
module->updateRateNames,
&module->updateRateIdx));

float updateRate = module->updateRates[module->updateRateIdx] / module->numActiveChannels;
menu->addChild(createMenuLabel(string::f("Per-channel MIDI update rate: %.3g Hz", updateRate)));
}
};


Model* modelMidiThing = createModel<MidiThing, MidiThingWidget>("MidiThingV2");

+ 0
- 224
src/PonyVCF.cpp View File

@@ -1,224 +0,0 @@
#include "plugin.hpp"

using simd::float_4;

// filter engine is just Fundemental VCF from https://github.com/VCVRack/Fundamental/blob/v2/src/VCF.cpp for now
// GPL-v3


template <typename T>
static T clip(T x) {
// return std::tanh(x);
// Pade approximant of tanh
x = simd::clamp(x, -3.f, 3.f);
return x * (27 + x * x) / (27 + 9 * x * x);
}


template <typename T>
struct LadderFilter {
T omega0;
T resonance = 1;
T state[4];
T input;

LadderFilter() {
reset();
setCutoff(0);
}

void reset() {
for (int i = 0; i < 4; i++) {
state[i] = 0;
}
}

void setCutoff(T cutoff) {
omega0 = 2 * T(M_PI) * cutoff;
}

void process(T input, T dt) {
dsp::stepRK4(T(0), dt, state, 4, [&](T t, const T x[], T dxdt[]) {
T inputt = crossfade(this->input, input, t / dt);
T inputc = clip(inputt - resonance * x[3]);
T yc0 = clip(x[0]);
T yc1 = clip(x[1]);
T yc2 = clip(x[2]);
T yc3 = clip(x[3]);

dxdt[0] = omega0 * (inputc - yc0);
dxdt[1] = omega0 * (yc0 - yc1);
dxdt[2] = omega0 * (yc1 - yc2);
dxdt[3] = omega0 * (yc2 - yc3);
});

this->input = input;
}

T lowpass() {
return state[3];
}
T highpass() {
return clip((input - resonance * state[3]) - 4 * state[0] + 6 * state[1] - 4 * state[2] + state[3]);
}
};


struct PonyVCF : Module {
enum ParamId {
CV1_PARAM,
RES_PARAM,
FREQ_PARAM,
GAIN1_PARAM,
GAIN2_PARAM,
GAIN3_PARAM,
ROUTING_PARAM,
PARAMS_LEN
};
enum InputId {
IN1_INPUT,
RES_INPUT,
VCA_INPUT,
IN2_INPUT,
CV1_INPUT,
IN3_INPUT,
CV2_INPUT,
INPUTS_LEN
};
enum OutputId {
OUTPUT,
OUTPUTS_LEN
};
enum LightId {
IN2_LIGHT,
IN1_LIGHT,
LIGHTS_LEN
};

LadderFilter<float_4> filters[4];

PonyVCF() {
config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN);
configParam(CV1_PARAM, 0.f, 1.f, 0.f, "CV1 Attenuator");
configParam(RES_PARAM, 0.f, 1.f, 0.f, "Resonance");
configParam(FREQ_PARAM, 0.f, 1.f, 0.f, "Frequency");
configParam(GAIN1_PARAM, 0.f, 1.2f, 1.f, "Gain Channel 1");
configParam(GAIN2_PARAM, 0.f, 1.2f, 1.f, "Gain Channel 2");
configParam(GAIN3_PARAM, 0.f, 1.2f, 1.f, "Gain Channel 3");
configParam(ROUTING_PARAM, 0.f, 1.f, 0.f, "VCA routing");

configInput(IN1_INPUT, "Channel 1");
configInput(RES_INPUT, "Resonance CV");
configInput(VCA_INPUT, "VCA");
configInput(IN2_INPUT, "Channel 2");
configInput(CV1_INPUT, "Frequency (CV1)");
configInput(IN3_INPUT, "Channel 3");
configInput(CV2_INPUT, "Frequency (CV2)");

configOutput(OUTPUT, "Main");

onReset();
}


void onReset() override {
for (int i = 0; i < 4; i++)
filters[i].reset();
}

float_4 prevOut[4] = {};

void process(const ProcessArgs& args) override {
if (!outputs[OUTPUT].isConnected()) {
return;
}

float resParam = params[RES_PARAM].getValue();
float freqParam = params[FREQ_PARAM].getValue();
float freqCvParam = params[CV1_PARAM].getValue();

int channels = std::max({1, inputs[IN1_INPUT].getChannels(), inputs[IN2_INPUT].getChannels(), inputs[IN3_INPUT].getChannels()});
for (int c = 0; c < channels; c += 4) {
auto& filter = filters[c / 4];

float_4 input = inputs[IN1_INPUT].getVoltageSimd<float_4>(c) * params[GAIN1_PARAM].getValue();
input += inputs[IN2_INPUT].getVoltageSimd<float_4>(c) * params[GAIN2_PARAM].getValue();
input += inputs[IN3_INPUT].getNormalVoltageSimd<float_4>(prevOut[c / 4], c) * params[GAIN3_PARAM].getValue();

// input = Saturator<float_4>::process(input / 5.0f) * 1.1f;
input = clip(input / 5.0f) * 1.1f;

// Add -120dB noise to bootstrap self-oscillation
input += 1e-6f * (2.f * random::uniform() - 1.f);

// Set resonance
float_4 resonance = resParam + inputs[RES_INPUT].getPolyVoltageSimd<float_4>(c) / 10.f;
resonance = clamp(resonance, 0.f, 1.f);
filter.resonance = simd::pow(resonance, 2) * 10.f;

// Get pitch
float_4 pitch = 5 * freqParam + inputs[CV1_INPUT].getPolyVoltageSimd<float_4>(c) * freqCvParam + inputs[CV2_INPUT].getPolyVoltageSimd<float_4>(c);
// Set cutoff
float_4 cutoff = dsp::FREQ_C4 * dsp::exp2_taylor5(pitch);
// Without oversampling, we must limit to 8000 Hz or so @ 44100 Hz
cutoff = clamp(cutoff, 1.f, args.sampleRate / 2.f);

// Without oversampling, we must limit to 8000 Hz or so @ 44100 Hz
cutoff = clamp(cutoff, 1.f, args.sampleRate * 0.18f);
filter.setCutoff(cutoff);

// Set outputs
filter.process(input, args.sampleTime);
if (outputs[OUTPUT].isConnected()) {
float_4 resGain = 1.0f / (0.05 + 0.9 * dsp::exp2_taylor5(-8.f * simd::pow(resonance, 2.0)));
// float_4 resGain = (1.0f + 8.f * resonance); // 1st order empirical fit
float_4 out = 5.f * filter.lowpass() * resGain;
outputs[OUTPUT].setVoltageSimd(out, c);

prevOut[c / 4] = out;
}

// DEBUG("channel %d %g", channels, input[0]);
}

outputs[OUTPUT].setChannels(channels);
}
};


struct PonyVCFWidget : ModuleWidget {
PonyVCFWidget(PonyVCF* module) {
setModule(module);
setPanel(createPanel(asset::plugin(pluginInstance, "res/panels/PonyVCF.svg")));

addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, 0)));
addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));

addParam(createParamCentered<BefacoTinyKnobDarkGrey>(mm2px(Vec(7.62, 14.5)), module, PonyVCF::CV1_PARAM));
addParam(createParamCentered<BefacoTinyKnobRed>(mm2px(Vec(22.38, 14.5)), module, PonyVCF::RES_PARAM));
addParam(createParamCentered<Davies1900hLargeGreyKnob>(mm2px(Vec(15.0, 35.001)), module, PonyVCF::FREQ_PARAM));
addParam(createParam<BefacoSlidePotSmall>(mm2px(Vec(3.217, 48.584)), module, PonyVCF::GAIN1_PARAM));
addParam(createParam<BefacoSlidePotSmall>(mm2px(Vec(13.271, 48.584)), module, PonyVCF::GAIN2_PARAM));
addParam(createParam<BefacoSlidePotSmall>(mm2px(Vec(23.316, 48.584)), module, PonyVCF::GAIN3_PARAM));
addParam(createParam<CKSSNarrow>(mm2px(Vec(23.498, 96.784)), module, PonyVCF::ROUTING_PARAM));

addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(5.0, 86.5)), module, PonyVCF::IN1_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(15.0, 86.5)), module, PonyVCF::RES_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(25.0, 86.5)), module, PonyVCF::VCA_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(5.0, 100.0)), module, PonyVCF::IN2_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(15.0, 100.0)), module, PonyVCF::CV1_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(5.0, 113.5)), module, PonyVCF::IN3_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(15.0, 113.5)), module, PonyVCF::CV2_INPUT));

addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(25.0, 113.5)), module, PonyVCF::OUTPUT));

addChild(createLightCentered<MediumLight<GreenLight>>(mm2px(Vec(2.578, 23.492)), module, PonyVCF::IN2_LIGHT));
addChild(createLightCentered<MediumLight<RedLight>>(mm2px(Vec(2.578, 27.159)), module, PonyVCF::IN1_LIGHT));
}
};


Model* modelPonyVCF = createModel<PonyVCF, PonyVCFWidget>("PonyVCF");

+ 0
- 3
src/plugin.cpp View File

@@ -7,7 +7,6 @@ void init(rack::Plugin *p) {
pluginInstance = p;

p->addModel(modelEvenVCO);
p->addModel(modelEvenVCO2);
p->addModel(modelRampage);
p->addModel(modelABC);
p->addModel(modelSpringReverb);
@@ -29,8 +28,6 @@ void init(rack::Plugin *p) {
p->addModel(modelPonyVCO);
p->addModel(modelMotionMTR);
p->addModel(modelBurst);
p->addModel(modelMidiThing);
p->addModel(modelVoltio);
p->addModel(modelOctaves);
p->addModel(modelPonyVCF);
}

+ 0
- 3
src/plugin.hpp View File

@@ -8,7 +8,6 @@ using namespace rack;
extern Plugin* pluginInstance;

extern Model* modelEvenVCO;
extern Model* modelEvenVCO2;
extern Model* modelRampage;
extern Model* modelABC;
extern Model* modelSpringReverb;
@@ -30,10 +29,8 @@ extern Model* modelChannelStrip;
extern Model* modelPonyVCO;
extern Model* modelMotionMTR;
extern Model* modelBurst;
extern Model* modelMidiThing;
extern Model* modelVoltio;
extern Model* modelOctaves;
extern Model* modelPonyVCF;

struct Knurlie : SvgScrew {
Knurlie() {


Loading…
Cancel
Save