| @@ -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() { | |||