Browse Source

Motion MTR (v2.4.0) (#42)

* Added MotionMTR 
* Improve Muxlicer docs and knob labels
* PonyVCO - update DC blocker to RC filter, avoids NaNs at high sample rates (fixes #41 #40 )

Fixes #40 #41
tags/v2.4.0^0
Ewan GitHub 2 years ago
parent
commit
fcf9b56401
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1971 additions and 9 deletions
  1. +10
    -0
      CHANGELOG.md
  2. +5
    -3
      README.md
  3. +23
    -0
      docs/Muxlicer.md
  4. BIN
      docs/img/MuxlicerIOMode.png
  5. BIN
      docs/img/MuxlicersChained.png
  6. +15
    -2
      plugin.json
  7. +88
    -0
      res/components/CKSSThree_bg.svg
  8. +148
    -0
      res/components/CKSSThree_fg.svg
  9. +1294
    -0
      res/panels/MotionMTR.svg
  10. +366
    -0
      src/MotionMTR.cpp
  11. +4
    -1
      src/Muxlicer.cpp
  12. +16
    -3
      src/PonyVCO.cpp
  13. +1
    -0
      src/plugin.cpp
  14. +1
    -0
      src/plugin.hpp

+ 10
- 0
CHANGELOG.md View File

@@ -1,5 +1,15 @@
# Change Log

## v2.4.0
* MotionMTR
* Initial release
* Muxlicer
* Fix gate labels
* Improve docs
* PonyVCO
* Fix NaNs when TZFM input is used at very high sample rates
* Fix pitch wobble when disconnecting TZFM voltage source

## v2.3.0
* PonyVCO
* Initial release


+ 5
- 3
README.md View File

@@ -17,13 +17,15 @@ We have tried to make the VCV implementations as authentic as possible, however

* Chopping Kinky hardward is DC coupled, but we add the option (default disabled) to remove this offset.

* The hardware Muxlicer assigns multiple functions to the "Speed Div/Mult" dial, that cannot be reproduced with a single mouse click. Some of these have been moved to the context menu, specifically: quadratic gates, the "All In" normalled voltage, and the input/output clock division/mult. The "Speed Div/Mult" dial remains only for main clock div/mult.
* See [docs/Muxlicer.md](docs/Muxlicer.md)

* The Noise Plethora filters self-oscillate on the hardware version but not the software version.

* EvenVCO has the option (default true) to remove DC from the pulse waveform output (hardware contains DC for non-50% duty cycles)
* EvenVCO has the option (default true) to remove DC from the pulse waveform output (hardware contains DC for non-50% duty cycles).

* PonyVCO optionally allows the user:
* to filter DC from the TZFM input signal (hardware filters below 15mHz)
* to limit the pulsewidth from 5% to 95% (hardware is full range)
* to remove DC from the pulse waveform output (hardware contains DC for non-50% duty cycles)
* to remove DC from the pulse waveform output (hardware contains DC for non-50% duty cycles)

* MotionMTR optionally doesn't use the 10V normalling on inputs if in audio mode to avoid acidentally adding unwanted DC to audio signals, see context menu. E.g. if you temporarily unpatch an audio source whilst using it it mixer mode, you get 10V DC suddenly and a nasty pop.

+ 23
- 0
docs/Muxlicer.md View File

@@ -0,0 +1,23 @@
# Muxlicer

The VCV port is designed to be as close as possible to the hardware unit, for which the manual can be found here: https://www.befaco.org/muxlicer-2/. This includes patch ideas and detailed notes.

> Muxlicer is a sequential signal processor designed for add a huge range of special functions to your modular setup in the minimum space. It is divided in three main blocks: a Digital Step Controller, a Gate Generator and an Analog Switch (a.k.a. Mux/DeMux). The module is designed with high “function to HP ratio” philosophy , to have maximum flexibility in minimum space.

## Differences from hardware version

The hardware Muxlicer assigns multiple functions to the "Speed Div/Mult" dial, that cannot be reproduced with a single mouse click. Some of these have been moved to the context menu, specifically: quadratic gates, the "All In" normalled voltage, and the input/output clock division/mult. The "Speed Div/Mult" dial remains only for main clock div/mult.

Tap tempo, which would be obtained by clicking the Speed Div/Mult dial in hardware, is now in the context menu.

The hardware uses a bidirectional sequential switch, meaning the IO jacks can be inputs or outputs. This is not possible with VCV, so a menu item has been added to allowing changing between "8 in - 1 out" and "1 in - 8 out" modes, see below. The jacks will change colour to indicate the change of mode.

![Muxlicer IO Mode](img/MuxlicerIOMode.png "Muxlicer IO Mode")


## Patch Ideas

You can chain multiple Muxlicers by linking the End of Cycle (EOC) outputs to the One-Shot/Reset outputs (see below). This works best when both units are externally clocked by the same clock source.

![Muxlicer Chaining](img/MuxlicersChained.png "Muxlicer Chaining")


BIN
docs/img/MuxlicerIOMode.png View File

Before After
Width: 1085  |  Height: 866  |  Size: 333KB

BIN
docs/img/MuxlicersChained.png View File

Before After
Width: 1093  |  Height: 709  |  Size: 365KB

+ 15
- 2
plugin.json View File

@@ -1,6 +1,6 @@
{
"slug": "Befaco",
"version": "2.3.0",
"version": "2.4.0",
"license": "GPL-3.0-or-later",
"name": "Befaco",
"brand": "Befaco",
@@ -208,7 +208,7 @@
"slug": "Muxlicer",
"name": "Muxlicer",
"description": "VC adressable sequential switch and sequencer",
"manualUrl": "https://www.befaco.org/muxlicer-2/",
"manualUrl": "https://github.com/VCVRack/Befaco/blob/v2/docs/Muxlicer.md",
"modularGridUrl": "https://www.modulargrid.net/e/befaco-muxlicer",
"tags": [
"Clock generator",
@@ -269,6 +269,19 @@
"Oscillator",
"Waveshaper"
]
},
{
"slug": "MotionMTR",
"name": "Motion MTR",
"description": "CV and audio utility / realtime visualizer",
"manualUrl": "https://www.befaco.org/motion_mtr/",
"modularGridUrl": "https://www.modulargrid.net/e/befaco-motion-mtr",
"tags": [
"Attenuator",
"Hardware clone",
"Mixer",
"Visual"
]
}
]
}

+ 88
- 0
res/components/CKSSThree_bg.svg View File

@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 25.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->

<svg
version="1.0"
id="svg258108"
sodipodi:docname="CKSSThree_bg.svg"
x="0px"
y="0px"
width="13.457px"
height="28.34766px"
viewBox="0 0 13.457 28.34766"
enable-background="new 0 0 13.457 28.34766"
xml:space="preserve"
inkscape:version="1.2.1 (9c6d41e, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs158" />
<sodipodi:namedview
bordercolor="#666666"
borderopacity="1.0"
fit-margin-bottom="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-top="0"
id="base"
inkscape:current-layer="svg258108"
inkscape:cx="3.8392857"
inkscape:cy="16.964286"
inkscape:document-units="mm"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:window-height="882"
inkscape:window-maximized="0"
inkscape:window-width="1600"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:zoom="11.2"
pagecolor="#ffffff"
showgrid="false"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1">
</sodipodi:namedview>
<path
id="path243332"
inkscape:connector-curvature="0"
d="M0,1.41795c0-0.78124,0.6406-1.41797,1.41793-1.41797h10.62499 c0.77739,0,1.4141,0.63671,1.4141,1.41797v25.51173c0,0.77735-0.63669,1.41797-1.4141,1.41797H1.41793 C0.6406,28.34765,0,27.70702,0,26.92968V1.41795z" />
<linearGradient
id="path243334_1_"
gradientUnits="userSpaceOnUse"
x1="6.73046"
y1="1.26953"
x2="6.73046"
y2="27.07812">
<stop
offset="0"
style="stop-color:#3B3B3B"
id="stop134" />
<stop
offset="1"
style="stop-color:#242424"
id="stop136" />
</linearGradient>
<path
id="path243334"
inkscape:connector-curvature="0"
opacity="0.54"
fill="url(#path243334_1_)"
d="M11.3242,1.26953H2.13671 c-0.48326,0-0.87501,0.39176-0.87501,0.87501v24.05857c0,0.48326,0.39176,0.87501,0.87501,0.87501h9.18749 c0.48326,0,0.87501-0.39176,0.87501-0.87501V2.14454C12.19921,1.66128,11.80746,1.26953,11.3242,1.26953z" />
<rect
x="-19.43386"
y="19.77311"
fill="none"
width="0.6829"
height="0.89523"
id="rect140" />
<rect
x="0.73033"
y="0.80507"
opacity="0.3"
width="12.00026"
height="1.71664"
id="rect142" />

</svg>

+ 148
- 0
res/components/CKSSThree_fg.svg View File

@@ -0,0 +1,148 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 25.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->

<svg
version="1.0"
id="svg258108"
sodipodi:docname="CKSSThree_fg.svg"
x="0px"
y="0px"
width="10.94143"
height="11.941"
viewBox="0 0 10.94143 11.941"
enable-background="new 0 0 13.457 28.34766"
xml:space="preserve"
inkscape:version="1.2.1 (9c6d41e, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs3211"><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1577"
id="linearGradient1579"
x1="6.4409394"
y1="10.29908"
x2="6.4493213"
y2="13.185839"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.003741,0,0,1,-0.03019806,-1.4171131)" /><linearGradient
inkscape:collect="always"
id="linearGradient1577"><stop
style="stop-color:#000000;stop-opacity:0.38225257;"
offset="0"
id="stop1573" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop1575" /></linearGradient></defs>
<sodipodi:namedview
bordercolor="#666666"
borderopacity="1.0"
fit-margin-bottom="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-top="0"
id="base"
inkscape:current-layer="svg258108"
inkscape:cx="1.0976869"
inkscape:cy="8.2185788"
inkscape:document-units="mm"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:window-height="882"
inkscape:window-maximized="0"
inkscape:window-width="1600"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:zoom="53.293886"
pagecolor="#ffffff"
showgrid="false"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1">
</sodipodi:namedview>

<linearGradient
id="path243334_1_"
gradientUnits="userSpaceOnUse"
x1="6.7304602"
y1="1.2695301"
x2="6.7304602"
y2="27.078119">
<stop
offset="0"
style="stop-color:#3B3B3B"
id="stop3187" />
<stop
offset="1"
style="stop-color:#242424"
id="stop3189" />
</linearGradient>

<rect
x="-19.433861"
y="19.773109"
fill="none"
width="0.68290001"
height="0.89523"
id="rect3193" />

<g
id="g3208"
transform="translate(-1.25778,-18.23161)">
<linearGradient
id="path5815_2_"
gradientUnits="userSpaceOnUse"
x1="6.7284999"
y1="18.231609"
x2="6.7284999"
y2="27.078119">
<stop
offset="0.00559"
style="stop-color:#3D3B3B"
id="stop3197" />
<stop
offset="1"
style="stop-color:#2D2C2C"
id="stop3199" />
</linearGradient>
<path
id="path5815_1_"
inkscape:connector-curvature="0"
fill="url(#path5815_2_)"
d="m 1.25778,18.23161 h 10.94143 v 8.84652 H 1.25778 Z"
style="fill:url(#path5815_2_)" />
<path
id="path5817_1_"
inkscape:connector-curvature="0"
fill="#5e5e5e"
d="m 1.25778,21.77853 h 10.94143 v 0.87738 H 1.25778 Z" />
<path
id="path5819_1_"
inkscape:connector-curvature="0"
fill="#5e5e5e"
d="m 1.25778,25.32545 h 10.94143 v 0.87738 H 1.25778 Z" />
<path
id="path5821_1_"
inkscape:connector-curvature="0"
fill="#5e5e5e"
d="m 1.25778,23.55285 h 10.94143 v 0.87738 H 1.25778 Z" />
<path
id="path5823_1_"
inkscape:connector-curvature="0"
fill="#5e5e5e"
d="m 1.25778,20.00513 h 10.94143 v 0.87738 H 1.25778 Z" />
<path
id="path5825_1_"
inkscape:connector-curvature="0"
fill="#797979"
d="m 1.25778,18.23161 h 10.94143 v 0.87738 H 1.25778 Z" />
</g>
<rect
style="fill:url(#linearGradient1579);fill-opacity:1;stroke:none;stroke-width:1.33393;stroke-linejoin:round;stroke-miterlimit:1.5"
id="rect1517"
width="13.160316"
height="2.9625583"
x="0"
y="8.8440666" /></svg>

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


+ 366
- 0
src/MotionMTR.cpp View File

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


struct MotionMTR : Module {
enum ParamId {
MODE1_PARAM,
CTRL_1_PARAM,
MODE2_PARAM,
CTRL_2_PARAM,
MODE3_PARAM,
CTRL_3_PARAM,
PARAMS_LEN
};
enum InputId {
IN1_INPUT,
IN2_INPUT,
IN3_INPUT,
INPUTS_LEN
};
enum OutputId {
OUT1_OUTPUT,
OUT2_OUTPUT,
OUT3_OUTPUT,
OUTPUTS_LEN
};
enum LightDisplayType {
CV_INV,
CV_ATT,
AUDIO
};

const std::vector<std::string> modeLabels = {"CV Inversion", "CV Attentuation", "Audio"};
static const int NUM_LIGHTS_PER_DIAL = 20;

enum LightId {
ENUMS(LIGHT_1, NUM_LIGHTS_PER_DIAL * 3),
ENUMS(LIGHT_2, NUM_LIGHTS_PER_DIAL * 3),
ENUMS(LIGHT_3, NUM_LIGHTS_PER_DIAL * 3),
LIGHTS_LEN
};

struct Map {
float dbValue;
long ledNumber;
};

const Map lut[20] = {
{ -30.5, 1}, { -30, 2}, { -30, 3}, { -26, 4}, { -25, 5}, { -24, 6}, { -23, 7 }, { -22, 8 }, { -21, 9}, { -20, 10},
{ -19, 11}, { -18, 12}, { -16, 13}, { -14, 14}, { -12, 15}, { -10, 16}, { -8, 17}, { -6, 18}, { -4, 19}, { -2, 20}
};

dsp::VuMeter2 vuBar[3];

const int updateLEDRate = 16;
dsp::ClockDivider sliderUpdate;

bool startingUp = true;
dsp::Timer startupTimer;

bool break10VNormalForAudioMode = true;

MotionMTR() {
config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN);

auto mode1 = configSwitch(MODE1_PARAM, 0.f, 2.f, 1.f, "Channel 1 mode", modeLabels);
mode1->snapEnabled = true;
configParam(CTRL_1_PARAM, 0.f, 1.f, 0.f, "Channel 1 gain");

auto mode2 = configSwitch(MODE2_PARAM, 0.f, 2.f, 1.f, "Channel 2 mode", modeLabels);
mode2->snapEnabled = true;
configParam(CTRL_2_PARAM, 0.f, 1.f, 0.f, "Channel 2 gain");

auto mode3 = configSwitch(MODE3_PARAM, 0.f, 2.f, 1.f, "Channel 3 mode", modeLabels);
mode3->snapEnabled = true;
configParam(CTRL_3_PARAM, 0.f, 1.f, 0.f, "Channel 3 gain");

auto in1 = configInput(IN1_INPUT, "Channel 1");
in1->description = "Normalled to 10V (except optionally in audio mode)";
auto in2 = configInput(IN2_INPUT, "Channel 2");
in2->description = "Normalled to 10V (except optionally in audio mode)";
auto in3 = configInput(IN3_INPUT, "Channel 3");
in3->description = "Normalled to 10V (except optionally in audio mode)";

configOutput(OUT1_OUTPUT, "Channel 1");
configOutput(OUT2_OUTPUT, "Channel 2");
configOutput(OUT3_OUTPUT, "Channel 3 (Mix)");

for (int i = 1; i < NUM_LIGHTS_PER_DIAL; ++i) {
configLight(LIGHT_1 + i * 3, string::f("%g to %g dB", lut[i - 1].dbValue, lut[i].dbValue));
configLight(LIGHT_2 + i * 3, string::f("%g to %g dB", lut[i - 1].dbValue, lut[i].dbValue));
configLight(LIGHT_3 + i * 3, string::f("%g to %g dB", lut[i - 1].dbValue, lut[i].dbValue));
}

for (int i = 0; i < 3; ++i) {
vuBar[i].mode = dsp::VuMeter2::PEAK;
vuBar[i].lambda = 10.f * updateLEDRate;
}

// only poll EQ sliders every 16 samples
sliderUpdate.setDivision(updateLEDRate);
// timer for startup animation
startupTimer.reset();
}

void onReset(const ResetEvent& e) override {
startingUp = true;
startupTimer.reset();
Module::onReset(e);
}

void process(const ProcessArgs& args) override {

const LightDisplayType mode1 = (LightDisplayType) params[MODE1_PARAM].getValue();
const LightDisplayType mode2 = (LightDisplayType) params[MODE2_PARAM].getValue();
const LightDisplayType mode3 = (LightDisplayType) params[MODE3_PARAM].getValue();

const float in1 = inputs[IN1_INPUT].getNormalVoltage(10.f * (mode1 != AUDIO || !break10VNormalForAudioMode));
const float in2 = inputs[IN2_INPUT].getNormalVoltage(10.f * (mode2 != AUDIO || !break10VNormalForAudioMode));
const float in3 = inputs[IN3_INPUT].getNormalVoltage(10.f * (mode3 != AUDIO || !break10VNormalForAudioMode));

const float out1 = in1 * params[CTRL_1_PARAM].getValue() * (mode1 == CV_INV ? -1 : +1);
const float out2 = in2 * params[CTRL_2_PARAM].getValue() * (mode2 == CV_INV ? -1 : +1);
float out3 = in3 * params[CTRL_3_PARAM].getValue() * (mode3 == CV_INV ? -1 : +1);

if (!outputs[OUT1_OUTPUT].isConnected()) {
out3 += out1;
}
if (!outputs[OUT2_OUTPUT].isConnected()) {
out3 += out2;
}

// special light pattern when starting up :)
if (startingUp) {
processStartup(args);
}
// otherwise (periodically) update LEDS according to value
else if (sliderUpdate.process()) {
lightsForSignal(mode1, LIGHT_1, out1, args, 0);
lightsForSignal(mode2, LIGHT_2, out2, args, 1);
lightsForSignal(mode3, LIGHT_3, out3, args, 2);
}

outputs[OUT1_OUTPUT].setVoltage(out1);
outputs[OUT2_OUTPUT].setVoltage(out2);
outputs[OUT3_OUTPUT].setVoltage(out3);
}

void processStartup(const ProcessArgs& args) {
float time = startupTimer.process(args.sampleTime);
const float ringTime = 0.4;

if (time < ringTime) {
int light = floor(NUM_LIGHTS_PER_DIAL * time / ringTime);
setLightHSBSmooth(LIGHT_1 + 3 * light, args, 360 * time / ringTime, 1., 1.);
}
else if (time < 2 * ringTime) {
time -= ringTime;
int light = floor(NUM_LIGHTS_PER_DIAL * time / ringTime);
setLightHSBSmooth(LIGHT_2 + 3 * light, args, 360 * time / ringTime, 1., 1.);
}
else if (time < 3 * ringTime) {
time -= 2 * ringTime;
int light = floor(NUM_LIGHTS_PER_DIAL * time / ringTime);
setLightHSBSmooth(LIGHT_3 + 3 * light, args, 360 * time / ringTime, 1., 1.);
}
else {
startingUp = false;
}
}

void setLightRGB(int lightId, float R, float G, float B) {
lights[lightId + 0].setBrightness(R);
lights[lightId + 1].setBrightness(G);
lights[lightId + 2].setBrightness(B);
}

void setLightRGBSmooth(int lightId, const ProcessArgs& args, float R, float G, float B) {
// inverse time constant for LED smoothing
const float lambda = 10.f * updateLEDRate;
lights[lightId + 0].setBrightnessSmooth(R, args.sampleTime, lambda);
lights[lightId + 1].setBrightnessSmooth(G, args.sampleTime, lambda);
lights[lightId + 2].setBrightnessSmooth(B, args.sampleTime, lambda);
}

// hue: 0 - 360
void setLightHSBSmooth(int lightId, const ProcessArgs& args, float H, float S, float V) {

float C = S * V;
float X = C * (1 - std::abs(std::fmod(H / 60.0, 2) - 1));
float m = V - C;
float r, g, b;
if (H >= 0 && H < 60) {
r = C, g = X, b = 0;
}
else if (H >= 60 && H < 120) {
r = X, g = C, b = 0;
}
else if (H >= 120 && H < 180) {
r = 0, g = C, b = X;
}
else if (H >= 180 && H < 240) {
r = 0, g = X, b = C;
}
else if (H >= 240 && H < 300) {
r = X, g = 0, b = C;
}
else {
r = C, g = 0, b = X;
}

float R = (r + m);
float G = (g + m);
float B = (b + m);

setLightRGBSmooth(lightId, args, R, G, B);
}

void lightsForSignal(LightDisplayType type, const LightId lightId, float signal, const ProcessArgs& args, const int channel) {

if (type == AUDIO) {
setLightRGB(lightId, 0.f, 1.0f, 0.f);

vuBar[channel].process(args.sampleTime * updateLEDRate, signal / 10.f);

for (int i = 1; i < NUM_LIGHTS_PER_DIAL; i++) {
const float value = vuBar[channel].getBrightness(lut[i - 1].dbValue, lut[i].dbValue);
if (i < 15) {
// green
setLightRGB(lightId + 3 * i, 0.f, value, 0.f);
}
else if (i < NUM_LIGHTS_PER_DIAL - 1) {
// yellow
setLightRGB(lightId + 3 * i, value, 0.65 * value, 0.f);
}
else {
// red
setLightRGB(lightId + 3 * i, value, 0.f, 0.f);
}
}
}
else {
setLightRGBSmooth(lightId, args, 0.82f, 0.0f, 0.82f);

if (signal >= 0) {
for (int i = 1; i < NUM_LIGHTS_PER_DIAL; ++i) {
float value = (signal > (10 * (i + 1.) / (NUM_LIGHTS_PER_DIAL + 1)));
// purple
setLightRGBSmooth(lightId + 3 * i, args, 0.82f * value, 0.0f, 0.82f * value);
}
}
else {
for (int i = 1; i < NUM_LIGHTS_PER_DIAL; ++i) {
float value = (signal < (-10 * (NUM_LIGHTS_PER_DIAL - i + 1.) / (NUM_LIGHTS_PER_DIAL + 1.)));
// orange
setLightRGBSmooth(lightId + 3 * i, args, value, 0.4f * value, 0.f);
}
}
}
}

json_t* dataToJson() override {
json_t* rootJ = json_object();
json_object_set_new(rootJ, "break10VNormalForAudioMode", json_boolean(break10VNormalForAudioMode));
return rootJ;
}

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

struct CKSSThreeDragable : app::SvgSlider {
CKSSThreeDragable() {
math::Vec margin = math::Vec(0, 0);
maxHandlePos = math::Vec(1, 1).plus(margin);
minHandlePos = math::Vec(1, 18).plus(margin);
setBackgroundSvg(Svg::load(asset::plugin(pluginInstance, "res/components/CKSSThree_bg.svg")));
setHandleSvg(Svg::load(asset::plugin(pluginInstance, "res/components/CKSSThree_fg.svg")));
background->box.pos = margin;
box.size = background->box.size.plus(margin.mult(2));
}

// disable double click as this messes with click to advance
void onDoubleClick(const event::DoubleClick& e) override { }

// cycle through the values (with reset) on click only (not drag)
void onAction(const ActionEvent& e) override {
ParamQuantity* paramQuantity = getParamQuantity();
float range = paramQuantity->maxValue - paramQuantity->minValue;
float newValue = paramQuantity->getValue() + 1.f;
if (newValue > paramQuantity->maxValue) {
newValue -= range + 1.f;
}
paramQuantity->setValue(newValue);
}
};


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

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

addParam(createParam<CKSSThreeDragable>(mm2px(Vec(1.298, 17.851)), module, MotionMTR::MODE1_PARAM));
addParam(createParamCentered<Davies1900hBlackKnob>(mm2px(Vec(18.217, 22.18)), module, MotionMTR::CTRL_1_PARAM));
addParam(createParam<CKSSThreeDragable>(mm2px(Vec(23.762, 46.679)), module, MotionMTR::MODE2_PARAM));
addParam(createParamCentered<Davies1900hBlackKnob>(mm2px(Vec(11.777, 50.761)), module, MotionMTR::CTRL_2_PARAM));
addParam(createParam<CKSSThreeDragable>(mm2px(Vec(1.34, 74.461)), module, MotionMTR::MODE3_PARAM));
addParam(createParamCentered<Davies1900hBlackKnob>(mm2px(Vec(18.31, 78.89)), module, MotionMTR::CTRL_3_PARAM));

addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(5.008, 100.315)), module, MotionMTR::IN1_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(14.993, 100.315)), module, MotionMTR::IN2_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(24.978, 100.315)), module, MotionMTR::IN3_INPUT));

addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(5.0, 113.207)), module, MotionMTR::OUT1_OUTPUT));
addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(14.978, 113.185)), module, MotionMTR::OUT2_OUTPUT));
addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(25.014, 113.207)), module, MotionMTR::OUT3_OUTPUT));


struct LightRingDetails {
MotionMTR::LightId startingId;
float x, y; // centre
} ;

std::vector<LightRingDetails> ringDetails = {
{MotionMTR::LIGHT_1, 18.217, 22.18},
{MotionMTR::LIGHT_2, 11.777, 50.761},
{MotionMTR::LIGHT_3, 18.217, 78.85}
};

const float R = 9.65; // mm
for (auto detailForRing : ringDetails) {
for (int i = 0; i < MotionMTR::NUM_LIGHTS_PER_DIAL; ++i) {
float theta = 2 * M_PI * i / MotionMTR::NUM_LIGHTS_PER_DIAL;
float x = detailForRing.x + sin(theta) * R;
float y = detailForRing.y - cos(theta) * R;
addChild(createLightCentered<SmallLight<RedGreenBlueLight>>(mm2px(Vec(x, y)), module, detailForRing.startingId + 3 * i));
}
}
}


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

menu->addChild(new MenuSeparator());
menu->addChild(createSubmenuItem("Hardware compatibility", "",
[ = ](Menu * menu) {
menu->addChild(createBoolPtrMenuItem("Disable 10V normal in audio mode", "", &module->break10VNormalForAudioMode));
}
));


}
};


Model* modelMotionMTR = createModel<MotionMTR, MotionMTRWidget>("MotionMTR");

+ 4
- 1
src/Muxlicer.cpp View File

@@ -322,7 +322,10 @@ struct Muxlicer : Module {
return "No gate";
}
else if (gate == 0) {
return "1/2 gate";
return "1 gate (100\% duty)";
}
else if (gate == 1) {
return "1 gate (50\% duty)";
}
else {
return string::f("%d gate(s)", gate);


+ 16
- 3
src/PonyVCO.cpp View File

@@ -49,6 +49,11 @@ public:
return x * x / 2.f;
}
}

void reset() {
xPrev = 0.f;
}

private:
float xPrev = 0.f;
};
@@ -101,6 +106,10 @@ public:
}
}

void reset() {
xPrev = 0.f;
}

private:
float xPrev = 0.f;
static constexpr float c = 0.1;
@@ -142,7 +151,7 @@ struct PonyVCO : Module {
chowdsp::VariableOversampling<6> oversampler; // uses a 2*6=12th order Butterworth filter
int oversamplingIndex = 1; // default is 2^oversamplingIndex == x2 oversampling

DCBlocker blockTZFMDCFilter;
dsp::RCFilter blockTZFMDCFilter;
bool blockTZFMDC = true;

// hardware doesn't limit PW but some user might want to (to 5%->95%)
@@ -182,9 +191,12 @@ struct PonyVCO : Module {

void onSampleRateChange() override {
float sampleRate = APP->engine->getSampleRate();
blockTZFMDCFilter.setFrequency(5. / sampleRate);
blockTZFMDCFilter.setCutoffFreq(5.0 / sampleRate);
oversampler.setOversamplingIndex(oversamplingIndex);
oversampler.reset(sampleRate);

stage1.reset();
stage2.reset();
}

// implementation taken from "Alias-Suppressed Oscillators Based on Differentiated Polynomial Waveforms",
@@ -209,7 +221,8 @@ struct PonyVCO : Module {

float tzfmVoltage = inputs[TZFM_INPUT].getVoltage();
if (blockTZFMDC) {
tzfmVoltage = blockTZFMDCFilter.process(tzfmVoltage);
blockTZFMDCFilter.process(tzfmVoltage);
tzfmVoltage = blockTZFMDCFilter.highpass();
}

const double pitch = inputs[VOCT_INPUT].getVoltage() + params[FREQ_PARAM].getValue() * range[rangeIndex];


+ 1
- 0
src/plugin.cpp View File

@@ -26,4 +26,5 @@ void init(rack::Plugin *p) {
p->addModel(modelNoisePlethora);
p->addModel(modelChannelStrip);
p->addModel(modelPonyVCO);
p->addModel(modelMotionMTR);
}

+ 1
- 0
src/plugin.hpp View File

@@ -27,6 +27,7 @@ extern Model* modelMex;
extern Model* modelNoisePlethora;
extern Model* modelChannelStrip;
extern Model* modelPonyVCO;
extern Model* modelMotionMTR;

struct Knurlie : SvgScrew {
Knurlie() {


Loading…
Cancel
Save