@@ -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 | |||
## 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 | |||
@@ -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" | |||
] | |||
} | |||
] | |||
} |
@@ -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; | |||
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); | |||
} |
@@ -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() { | |||