@@ -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 |
@@ -1,11 +1,11 @@ | |||||
# Change Log | # Change Log | ||||
## v2.6.0 (in progress) | |||||
* Midi Thing 2 | |||||
* Initial release | |||||
## v2.6.0 | |||||
* Octaves | * Octaves | ||||
* Initial release | * Initial release | ||||
* Misc | |||||
* Better default values for ADSR and Burst | |||||
## v2.5.0 | ## v2.5.0 | ||||
@@ -23,18 +23,6 @@ | |||||
"Polyphonic" | "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", | "slug": "Rampage", | ||||
"name": "Rampage", | "name": "Rampage", | ||||
@@ -308,18 +296,6 @@ | |||||
"Hardware clone" | "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", | "slug": "Voltio", | ||||
"name": "Voltio", | "name": "Voltio", | ||||
@@ -342,18 +318,6 @@ | |||||
"Hardware clone", | "Hardware clone", | ||||
"VCO" | "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" | |||||
] | |||||
} | } | ||||
] | ] | ||||
} | } |
@@ -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"); |
@@ -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"); |
@@ -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"); |
@@ -7,7 +7,6 @@ void init(rack::Plugin *p) { | |||||
pluginInstance = p; | pluginInstance = p; | ||||
p->addModel(modelEvenVCO); | p->addModel(modelEvenVCO); | ||||
p->addModel(modelEvenVCO2); | |||||
p->addModel(modelRampage); | p->addModel(modelRampage); | ||||
p->addModel(modelABC); | p->addModel(modelABC); | ||||
p->addModel(modelSpringReverb); | p->addModel(modelSpringReverb); | ||||
@@ -29,8 +28,6 @@ void init(rack::Plugin *p) { | |||||
p->addModel(modelPonyVCO); | p->addModel(modelPonyVCO); | ||||
p->addModel(modelMotionMTR); | p->addModel(modelMotionMTR); | ||||
p->addModel(modelBurst); | p->addModel(modelBurst); | ||||
p->addModel(modelMidiThing); | |||||
p->addModel(modelVoltio); | p->addModel(modelVoltio); | ||||
p->addModel(modelOctaves); | p->addModel(modelOctaves); | ||||
p->addModel(modelPonyVCF); | |||||
} | } |
@@ -8,7 +8,6 @@ using namespace rack; | |||||
extern Plugin* pluginInstance; | extern Plugin* pluginInstance; | ||||
extern Model* modelEvenVCO; | extern Model* modelEvenVCO; | ||||
extern Model* modelEvenVCO2; | |||||
extern Model* modelRampage; | extern Model* modelRampage; | ||||
extern Model* modelABC; | extern Model* modelABC; | ||||
extern Model* modelSpringReverb; | extern Model* modelSpringReverb; | ||||
@@ -30,10 +29,8 @@ extern Model* modelChannelStrip; | |||||
extern Model* modelPonyVCO; | extern Model* modelPonyVCO; | ||||
extern Model* modelMotionMTR; | extern Model* modelMotionMTR; | ||||
extern Model* modelBurst; | extern Model* modelBurst; | ||||
extern Model* modelMidiThing; | |||||
extern Model* modelVoltio; | extern Model* modelVoltio; | ||||
extern Model* modelOctaves; | extern Model* modelOctaves; | ||||
extern Model* modelPonyVCF; | |||||
struct Knurlie : SvgScrew { | struct Knurlie : SvgScrew { | ||||
Knurlie() { | Knurlie() { | ||||