diff --git a/CHANGELOG.md b/CHANGELOG.md index 26a2f9f..4bd0bc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## v1.2.0 (unreleased) + + * Released new modules: Muxlicer, Mex, Morphader, VC ADSR, Sampling Modulator, ST Mix + * Removed DC offset from EvenVCO pulse output + ## v1.1.1 * Fixed issue with A*B+C summing logic diff --git a/README.md b/README.md index c69463a..69eee2d 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,17 @@ Based on [Befaco](http://www.befaco.org/) Eurorack modules. -[VCV Library page](https://library.vcvrack.com/Befaco) \ No newline at end of file +[VCV Library page](https://library.vcvrack.com/Befaco) + + +## Differences with hardware + +We have tried to make the VCV implementations as authentic as possible, however there are some minor changes that have been made (either for usuability or to bring modules in line with VCV rack conventions and standards). + +* Sampling Modulator removes the DC offset of "Clock Out" and "Trigg Out" to allow these to be easily used as oscillators - the legacy behaviour (0 - 10V pulses) can be selected via the context menu. + +* The hardware version of Morphader accepts 0-8V CV for the crossfade control, here we widen this to accept 0-10V. + +* 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. \ No newline at end of file diff --git a/plugin.json b/plugin.json index 42c4844..923da00 100644 --- a/plugin.json +++ b/plugin.json @@ -3,7 +3,7 @@ "version": "1.1.1", "license": "GPL-3.0-or-later", "name": "Befaco", - "author": "VCV", + "author": "VCV, Ewan Hemingway", "authorEmail": "contact@vcvrack.com", "pluginUrl": "https://vcvrack.com/Befaco", "authorUrl": "https://vcvrack.com/", @@ -111,7 +111,8 @@ "Envelope generator", "Mixer", "Polyphonic", - "Hardware clone" + "Hardware clone", + "Quad" ] }, { @@ -151,6 +152,80 @@ "Hardware clone", "Synth voice" ] + }, + { + "slug": "SamplingModulator", + "name": "Sampling Modulator", + "description": "Multi-function module that lies somewhere between a VCO, a Sample & Hold, and an 8 step trigger sequencer", + "manualUrl": "https://www.befaco.org/sampling-modulator/", + "modularGridUrl": "https://www.modulargrid.net/e/befaco-sampling-modulator-", + "tags": [ + "Clock generator", + "Hardware clone", + "Oscillator", + "Sample and hold" + ] + }, + { + "slug": "Morphader", + "name": "Morphader", + "description": "Multichannel CV/Audio crossfader", + "manualUrl": "https://www.befaco.org/morphader-2/", + "modularGridUrl": "https://www.modulargrid.net/e/befaco-morphader", + "tags": [ + "Controller", + "Hardware clone", + "Mixer", + "Polyphonic", + "Quad" + ] + }, + { + "slug": "ADSR", + "name": "ADSR", + "description": "ADSR envelope generator with gate output on each stage, plus variable shape", + "manualUrl": "https://www.befaco.org/vc-adsr/", + "modularGridUrl": "https://www.modulargrid.net/e/befaco-vc-adsr", + "tags": [ + "Envelope generator", + "Hardware clone" + ] + }, + { + "slug": "STMix", + "name": "STMix", + "description": "A compact 4 channel stereo mixer with an auxiliary input", + "manualUrl": "https://www.befaco.org/stmix/", + "modularGridUrl": "https://www.modulargrid.net/e/befaco-stmix-", + "tags": [ + "Hardware clone", + "Mixer", + "Stereo", + "Polyphonic" + ] + }, + { + "slug": "Muxlicer", + "name": "Muxlicer", + "description": "VC adressable sequential switch and sequencer", + "manualUrl": "https://www.befaco.org/muxlicer-2/", + "modularGridUrl": "https://www.modulargrid.net/e/befaco-muxlicer", + "tags": [ + "Clock generator", + "Hardware clone", + "Polyphonic", + "Sequencer", + "Switch" + ] + }, + { + "slug": "Mex", + "name": "Mex", + "description": "Gate Expander for Befaco Muxlicer", + "tags": [ + "Expander", + "Hardware clone" + ] } ] -} +} \ No newline at end of file diff --git a/res/ADSR.svg b/res/ADSR.svg new file mode 100644 index 0000000..5b5d030 --- /dev/null +++ b/res/ADSR.svg @@ -0,0 +1,807 @@ + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/BefacoButton_0.svg b/res/BefacoButton_0.svg new file mode 100644 index 0000000..6429238 --- /dev/null +++ b/res/BefacoButton_0.svg @@ -0,0 +1,72 @@ + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/res/BefacoButton_1.svg b/res/BefacoButton_1.svg new file mode 100644 index 0000000..483d36f --- /dev/null +++ b/res/BefacoButton_1.svg @@ -0,0 +1,72 @@ + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/res/BefacoSwitchHoriz_0.svg b/res/BefacoSwitchHoriz_0.svg new file mode 100644 index 0000000..27ea795 --- /dev/null +++ b/res/BefacoSwitchHoriz_0.svg @@ -0,0 +1,101 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/res/BefacoSwitchHoriz_1.svg b/res/BefacoSwitchHoriz_1.svg new file mode 100644 index 0000000..15c8b6b --- /dev/null +++ b/res/BefacoSwitchHoriz_1.svg @@ -0,0 +1,86 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/res/BefacoSwitchHoriz_2.svg b/res/BefacoSwitchHoriz_2.svg new file mode 100644 index 0000000..dfc5355 --- /dev/null +++ b/res/BefacoSwitchHoriz_2.svg @@ -0,0 +1,104 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/res/BefacoTinyKnobBlack.svg b/res/BefacoTinyKnobBlack.svg new file mode 100644 index 0000000..c72289e --- /dev/null +++ b/res/BefacoTinyKnobBlack.svg @@ -0,0 +1,85 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/res/BefacoTinyKnobGrey.svg b/res/BefacoTinyKnobDarkGrey.svg similarity index 100% rename from res/BefacoTinyKnobGrey.svg rename to res/BefacoTinyKnobDarkGrey.svg diff --git a/res/BefacoTinyKnobLightGrey.svg b/res/BefacoTinyKnobLightGrey.svg new file mode 100644 index 0000000..8776e38 --- /dev/null +++ b/res/BefacoTinyKnobLightGrey.svg @@ -0,0 +1,85 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/res/CrossfaderBackground.svg b/res/CrossfaderBackground.svg new file mode 100644 index 0000000..fbd75d1 --- /dev/null +++ b/res/CrossfaderBackground.svg @@ -0,0 +1,85 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/res/CrossfaderHandle.svg b/res/CrossfaderHandle.svg new file mode 100644 index 0000000..c21b10e --- /dev/null +++ b/res/CrossfaderHandle.svg @@ -0,0 +1,113 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/Davies1900hBlack.svg b/res/Davies1900hBlack.svg new file mode 100644 index 0000000..459f596 --- /dev/null +++ b/res/Davies1900hBlack.svg @@ -0,0 +1,105 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/res/Davies1900hDarkGrey.svg b/res/Davies1900hDarkGrey.svg new file mode 100644 index 0000000..a2e6294 --- /dev/null +++ b/res/Davies1900hDarkGrey.svg @@ -0,0 +1,105 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/res/Davies1900hLightGrey.svg b/res/Davies1900hLightGrey.svg new file mode 100644 index 0000000..ceac137 --- /dev/null +++ b/res/Davies1900hLightGrey.svg @@ -0,0 +1,105 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/res/Mex.svg b/res/Mex.svg new file mode 100644 index 0000000..b3024b4 --- /dev/null +++ b/res/Mex.svg @@ -0,0 +1,541 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/Morphader.svg b/res/Morphader.svg new file mode 100644 index 0000000..7fbccce --- /dev/null +++ b/res/Morphader.svg @@ -0,0 +1,1751 @@ + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 4 + 3 + 2 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/Muxlicer.svg b/res/Muxlicer.svg new file mode 100644 index 0000000..e70ac12 --- /dev/null +++ b/res/Muxlicer.svg @@ -0,0 +1,5901 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/STMix.svg b/res/STMix.svg new file mode 100644 index 0000000..52ac16d --- /dev/null +++ b/res/STMix.svg @@ -0,0 +1,704 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + +   + + + + + + + + + + + + + +   +   + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +   + + + diff --git a/res/SamplingModulator.svg b/res/SamplingModulator.svg new file mode 100644 index 0000000..5dbdeae --- /dev/null +++ b/res/SamplingModulator.svg @@ -0,0 +1,1063 @@ + + + + + + + + image/svg+xml + + + + + + + + + +   + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/SwitchNarrow_0.svg b/res/SwitchNarrow_0.svg new file mode 100644 index 0000000..b5f0c18 --- /dev/null +++ b/res/SwitchNarrow_0.svg @@ -0,0 +1,114 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/SwitchNarrow_1.svg b/res/SwitchNarrow_1.svg new file mode 100644 index 0000000..b71fd86 --- /dev/null +++ b/res/SwitchNarrow_1.svg @@ -0,0 +1,114 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ABC.cpp b/src/ABC.cpp index 3ef6ecf..cc6c6f1 100644 --- a/src/ABC.cpp +++ b/src/ABC.cpp @@ -11,13 +11,6 @@ static T clip(T x) { / (1.0f + 1.54167f * simd::pow(x, 12) + 0.642361f * simd::pow(x, 24) + 0.0579909f * simd::pow(x, 36)); } - -static float exponentialBipolar80Pade_5_4(float x) { - return (0.109568 * x + 0.281588 * std::pow(x, 3) + 0.133841 * std::pow(x, 5)) - / (1. - 0.630374 * std::pow(x, 2) + 0.166271 * std::pow(x, 4)); -} - - struct ABC : Module { enum ParamIds { B1_LEVEL_PARAM, diff --git a/src/ADSR.cpp b/src/ADSR.cpp new file mode 100644 index 0000000..c4824e7 --- /dev/null +++ b/src/ADSR.cpp @@ -0,0 +1,341 @@ +#include "plugin.hpp" + + +struct BefacoADSREnvelope { + + enum Stage { + STAGE_OFF, + STAGE_ATTACK, + STAGE_DECAY, + STAGE_SUSTAIN, + STAGE_RELEASE + }; + + Stage stage = STAGE_OFF; + float env = 0.f; + float releaseValue; + float timeInCurrentStage = 0.f; + float attackTime = 0.1, decayTime = 0.1, releaseTime = 0.1; + float attackShape = 1.0, decayShape = 1.0, releaseShape = 1.0; + float sustainLevel; + + BefacoADSREnvelope() { }; + + void retrigger() { + stage = STAGE_ATTACK; + // get the linear value of the envelope + timeInCurrentStage = attackTime * std::pow(env, 1.0f / attackShape); + } + + void processTransitionsGateMode(const bool& gateHeld) { + if (gateHeld) { + // calculate stage transitions + switch (stage) { + case STAGE_OFF: { + env = 0.0f; + timeInCurrentStage = 0.f; + stage = STAGE_ATTACK; + break; + } + case STAGE_ATTACK: { + if (env >= 1.f) { + timeInCurrentStage = 0.f; + stage = STAGE_DECAY; + } + break; + } + case STAGE_DECAY: { + if (timeInCurrentStage >= decayTime) { + timeInCurrentStage = 0.f; + stage = STAGE_SUSTAIN; + } + break; + } + case STAGE_SUSTAIN: { + break; + } + case STAGE_RELEASE: { + stage = STAGE_ATTACK; + timeInCurrentStage = attackTime * env; + break; + } + } + } + else { + if (stage == STAGE_ATTACK || stage == STAGE_DECAY || stage == STAGE_SUSTAIN) { + timeInCurrentStage = 0.f; + stage = STAGE_RELEASE; + releaseValue = env; + } + else if (stage == STAGE_RELEASE) { + if (timeInCurrentStage >= releaseTime) { + stage = STAGE_OFF; + timeInCurrentStage = 0.f; + } + } + } + } + + void processTransitionsTriggerMode(const bool& gateHeld) { + + // calculate stage transitions + switch (stage) { + case STAGE_ATTACK: { + if (env >= 1.f) { + timeInCurrentStage = 0.f; + if (gateHeld) { + stage = STAGE_DECAY; + } + else { + stage = STAGE_RELEASE; + releaseValue = 1.f; + } + } + break; + } + case STAGE_DECAY: { + if (timeInCurrentStage >= decayTime) { + timeInCurrentStage = 0.f; + if (gateHeld) { + stage = STAGE_SUSTAIN; + } + else { + stage = STAGE_RELEASE; + releaseValue = env; + } + } + break; + } + case STAGE_OFF: + case STAGE_RELEASE: + case STAGE_SUSTAIN: { + break; + } + } + + if (!gateHeld) { + if (stage == STAGE_DECAY || stage == STAGE_SUSTAIN) { + timeInCurrentStage = 0.f; + stage = STAGE_RELEASE; + releaseValue = env; + } + else if (stage == STAGE_RELEASE) { + if (timeInCurrentStage >= releaseTime) { + stage = STAGE_OFF; + timeInCurrentStage = 0.f; + } + } + } + } + + void evolveEnvelope(const float& sampleTime) { + switch (stage) { + case STAGE_OFF: { + env = 0.0f; + break; + } + case STAGE_ATTACK: { + timeInCurrentStage += sampleTime; + env = std::min(timeInCurrentStage / attackTime, 1.f); + env = std::pow(env, attackShape); + break; + } + case STAGE_DECAY: { + timeInCurrentStage += sampleTime; + env = std::pow(1.f - std::min(1.f, timeInCurrentStage / decayTime), decayShape); + env = sustainLevel + (1.f - sustainLevel) * env; + break; + } + case STAGE_SUSTAIN: { + env = sustainLevel; + break; + } + case STAGE_RELEASE: { + timeInCurrentStage += sampleTime; + env = std::min(1.0f, timeInCurrentStage / releaseTime); + env = releaseValue * std::pow(1.0f - env, releaseShape); + break; + } + } + } + + void process(const float& sampleTime, const bool& gateHeld, const bool& triggerMode) { + + if (triggerMode) { + processTransitionsTriggerMode(gateHeld); + } + else { + processTransitionsGateMode(gateHeld); + } + + evolveEnvelope(sampleTime); + } +}; + +struct ADSR : Module { + enum ParamIds { + TRIGG_GATE_TOGGLE_PARAM, + MANUAL_TRIGGER_PARAM, + SHAPE_PARAM, + ATTACK_PARAM, + DECAY_PARAM, + SUSTAIN_PARAM, + RELEASE_PARAM, + NUM_PARAMS + }; + enum InputIds { + TRIGGER_INPUT, + CV_ATTACK_INPUT, + CV_DECAY_INPUT, + CV_SUSTAIN_INPUT, + CV_RELEASE_INPUT, + NUM_INPUTS + }; + enum OutputIds { + OUT_OUTPUT, + STAGE_ATTACK_OUTPUT, + STAGE_DECAY_OUTPUT, + STAGE_SUSTAIN_OUTPUT, + STAGE_RELEASE_OUTPUT, + NUM_OUTPUTS + }; + enum LightIds { + LED_LIGHT, + LED_ATTACK_LIGHT, + LED_DECAY_LIGHT, + LED_SUSTAIN_LIGHT, + LED_RELEASE_LIGHT, + NUM_LIGHTS + }; + enum EnvelopeMode { + GATE_MODE, + TRIGGER_MODE + }; + + BefacoADSREnvelope envelope; + dsp::SchmittTrigger gateTrigger; + dsp::ClockDivider cvDivider; + float shape; + + static constexpr float minStageTime = 0.003f; // in seconds + static constexpr float maxStageTime = 10.f; // in seconds + + // given a value from the slider and/or cv (rescaled to range 0 to 1), transform into the appropriate time in seconds + static float convertCVToTimeInSeconds(float cv) { + return minStageTime * std::pow(maxStageTime / minStageTime, cv); + } + + struct TriggerGateParamQuantity : ParamQuantity { + std::string getDisplayValueString() override { + switch ((EnvelopeMode) getValue()) { + case ADSR::GATE_MODE: return "Gate"; + case ADSR::TRIGGER_MODE: return "Trigger"; + default: assert(false); + } + } + }; + + ADSR() { + config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); + configParam(TRIGG_GATE_TOGGLE_PARAM, GATE_MODE, TRIGGER_MODE, GATE_MODE, "Mode"); + configParam(MANUAL_TRIGGER_PARAM, 0.f, 1.f, 0.f, "Trigger envelope"); + configParam(SHAPE_PARAM, 0.f, 1.f, 0.f, "Envelope shape"); + + configParam(ATTACK_PARAM, 0.f, 1.f, 0.f, "Attack time", "s", maxStageTime / minStageTime, minStageTime); + configParam(DECAY_PARAM, 0.f, 1.f, 0.f, "Decay time", "s", maxStageTime / minStageTime, minStageTime); + configParam(SUSTAIN_PARAM, 0.f, 1.f, 0.f, "Sustain level", "%", 0.f, 100.f); + configParam(RELEASE_PARAM, 0.f, 1.f, 0.f, "Release time", "s", maxStageTime / minStageTime, minStageTime); + + cvDivider.setDivision(16); + } + + void process(const ProcessArgs& args) override { + + + if (cvDivider.process()) { + shape = params[SHAPE_PARAM].getValue(); + envelope.decayShape = 1.f + shape; + envelope.attackShape = 1.f - shape / 2.f; + envelope.releaseShape = 1.f + shape; + + const float attackCV = clamp(params[ATTACK_PARAM].getValue() + inputs[CV_ATTACK_INPUT].getVoltage() / 10.f, 0.f, 1.f); + envelope.attackTime = convertCVToTimeInSeconds(attackCV); + + const float decayCV = clamp(params[DECAY_PARAM].getValue() + inputs[CV_DECAY_INPUT].getVoltage() / 10.f, 0.f, 1.f); + envelope.decayTime = convertCVToTimeInSeconds(decayCV); + + const float sustainCV = clamp(params[SUSTAIN_PARAM].getValue() + inputs[CV_SUSTAIN_INPUT].getVoltage() / 10.f, 0.f, 1.f); + envelope.sustainLevel = sustainCV; + + const float releaseCV = clamp(params[RELEASE_PARAM].getValue() + inputs[CV_RELEASE_INPUT].getVoltage() / 10.f, 0.f, 1.f); + envelope.releaseTime = convertCVToTimeInSeconds(releaseCV); + } + + const bool triggered = gateTrigger.process(rescale(params[MANUAL_TRIGGER_PARAM].getValue() * 10.f + inputs[TRIGGER_INPUT].getVoltage(), 0.1f, 2.f, 0.f, 1.f)); + const bool gateOn = gateTrigger.isHigh() || params[MANUAL_TRIGGER_PARAM].getValue(); + const bool triggerMode = params[TRIGG_GATE_TOGGLE_PARAM].getValue() == 1; + + if (triggerMode) { + if (triggered) { + envelope.retrigger(); + } + } + + envelope.process(args.sampleTime, gateOn, triggerMode); + + outputs[OUT_OUTPUT].setVoltage(envelope.env * 10.f); + + outputs[STAGE_ATTACK_OUTPUT].setVoltage(10.f * (envelope.stage == BefacoADSREnvelope::STAGE_ATTACK)); + outputs[STAGE_DECAY_OUTPUT].setVoltage(10.f * (envelope.stage == BefacoADSREnvelope::STAGE_DECAY)); + outputs[STAGE_SUSTAIN_OUTPUT].setVoltage(10.f * (envelope.stage == BefacoADSREnvelope::STAGE_SUSTAIN)); + outputs[STAGE_RELEASE_OUTPUT].setVoltage(10.f * (envelope.stage == BefacoADSREnvelope::STAGE_RELEASE)); + + lights[LED_ATTACK_LIGHT].setBrightness((envelope.stage == BefacoADSREnvelope::STAGE_ATTACK)); + lights[LED_DECAY_LIGHT].setBrightness((envelope.stage == BefacoADSREnvelope::STAGE_DECAY)); + lights[LED_SUSTAIN_LIGHT].setBrightness((envelope.stage == BefacoADSREnvelope::STAGE_SUSTAIN)); + lights[LED_RELEASE_LIGHT].setBrightness((envelope.stage == BefacoADSREnvelope::STAGE_RELEASE)); + lights[LED_LIGHT].setBrightness((float) gateOn); + } +}; + + +struct ADSRWidget : ModuleWidget { + ADSRWidget(ADSR* module) { + setModule(module); + setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/ADSR.svg"))); + + addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); + addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); + addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + + addParam(createParamCentered(mm2px(Vec(20.263, 17.128)), module, ADSR::TRIGG_GATE_TOGGLE_PARAM)); + addParam(createParamCentered(mm2px(Vec(11.581, 32.473)), module, ADSR::MANUAL_TRIGGER_PARAM)); + addParam(createParamCentered(mm2px(Vec(29.063, 32.573)), module, ADSR::SHAPE_PARAM)); + addParam(createParam(mm2px(Vec(2.294, 45.632)), module, ADSR::ATTACK_PARAM)); + addParam(createParam(mm2px(Vec(12.422, 45.632)), module, ADSR::DECAY_PARAM)); + addParam(createParam(mm2px(Vec(22.551, 45.632)), module, ADSR::SUSTAIN_PARAM)); + addParam(createParam(mm2px(Vec(32.68, 45.632)), module, ADSR::RELEASE_PARAM)); + + addInput(createInputCentered(mm2px(Vec(6.841, 15.5)), module, ADSR::TRIGGER_INPUT)); + addInput(createInputCentered(mm2px(Vec(5.022, 113.506)), module, ADSR::CV_ATTACK_INPUT)); + addInput(createInputCentered(mm2px(Vec(15.195, 113.506)), module, ADSR::CV_DECAY_INPUT)); + addInput(createInputCentered(mm2px(Vec(25.368, 113.506)), module, ADSR::CV_SUSTAIN_INPUT)); + addInput(createInputCentered(mm2px(Vec(35.541, 113.506)), module, ADSR::CV_RELEASE_INPUT)); + + addOutput(createOutputCentered(mm2px(Vec(33.721, 15.479)), module, ADSR::OUT_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(5.022, 100.858)), module, ADSR::STAGE_ATTACK_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(15.195, 100.858)), module, ADSR::STAGE_DECAY_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(25.368, 100.858)), module, ADSR::STAGE_SUSTAIN_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(35.541, 100.858)), module, ADSR::STAGE_RELEASE_OUTPUT)); + + addChild(createLightCentered>(mm2px(Vec(20.254, 40.864)), module, ADSR::LED_LIGHT)); + addChild(createLightCentered>(mm2px(Vec(5.001, 92.893)), module, ADSR::LED_ATTACK_LIGHT)); + addChild(createLightCentered>(mm2px(Vec(15.174, 92.893)), module, ADSR::LED_DECAY_LIGHT)); + addChild(createLightCentered>(mm2px(Vec(25.347, 92.893)), module, ADSR::LED_SUSTAIN_LIGHT)); + addChild(createLightCentered>(mm2px(Vec(35.52, 92.893)), module, ADSR::LED_RELEASE_LIGHT)); + } +}; + + +Model* modelADSR = createModel("ADSR"); \ No newline at end of file diff --git a/src/ChoppingKinky.cpp b/src/ChoppingKinky.cpp index 65f9a93..512a8c2 100644 --- a/src/ChoppingKinky.cpp +++ b/src/ChoppingKinky.cpp @@ -90,13 +90,13 @@ struct ChoppingKinky : Module { gainB += inputs[VCA_CV_B_INPUT].getVoltage() / 10.f; gainB = std::max(gainB, 0.f); - const float inA = inputs[IN_A_INPUT].getVoltage(); - const float inB = inputs[IN_B_INPUT].getNormalVoltage(inputs[IN_A_INPUT].getVoltage()); + const float inA = inputs[IN_A_INPUT].getVoltageSum(); + const float inB = inputs[IN_B_INPUT].getNormalVoltage(inputs[IN_A_INPUT].getVoltageSum()); // if the CHOPP gate is wired in, do chop logic if (inputs[IN_GATE_INPUT].isConnected()) { // TODO: check rescale? - trigger.process(rescale(inputs[IN_GATE_INPUT].getVoltage(), 0.1f, 2.f, 0.f, 1.f)); + trigger.process(rescale(inputs[IN_GATE_INPUT].getVoltageSum(), 0.1f, 2.f, 0.f, 1.f)); outputAToChopp = trigger.isHigh(); } // else zero-crossing detector on input A switches between A and B diff --git a/src/EvenVCO.cpp b/src/EvenVCO.cpp index fa2c4b3..2d0bf4a 100644 --- a/src/EvenVCO.cpp +++ b/src/EvenVCO.cpp @@ -86,7 +86,7 @@ struct EvenVCO : Module { freq[c / 4] = clamp(freq[c / 4], 0.f, 20000.f); } - // Pulse width + // Pulse width float_4 pw[4] = {}; for (int c = 0; c < channels; c += 4) pw[c / 4] = params[PWM_PARAM].getValue(); @@ -95,7 +95,7 @@ struct EvenVCO : Module { for (int c = 0; c < channels; c += 4) pw[c / 4] += inputs[PWM_INPUT].getPolyVoltageSimd(c) / 5.f; } - + float_4 deltaPhase[4] = {}; float_4 oldPhase[4] = {}; for (int c = 0; c < channels; c += 4) { diff --git a/src/Kickall.cpp b/src/Kickall.cpp index 6be5259..a97fb13 100644 --- a/src/Kickall.cpp +++ b/src/Kickall.cpp @@ -123,7 +123,7 @@ struct KickallWidget : ModuleWidget { addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); - addParam(createParamCentered(mm2px(Vec(8.472, 28.97)), module, Kickall::TUNE_PARAM)); + addParam(createParamCentered(mm2px(Vec(8.472, 28.97)), module, Kickall::TUNE_PARAM)); addParam(createParamCentered(mm2px(Vec(22.409, 29.159)), module, Kickall::TRIGG_BUTTON_PARAM)); addParam(createParamCentered(mm2px(Vec(15.526, 49.292)), module, Kickall::SHAPE_PARAM)); addParam(createParam(mm2px(Vec(19.667, 63.897)), module, Kickall::DECAY_PARAM)); diff --git a/src/Mex.cpp b/src/Mex.cpp new file mode 100644 index 0000000..f69b253 --- /dev/null +++ b/src/Mex.cpp @@ -0,0 +1,142 @@ +#include "plugin.hpp" +#include "Muxlicer.hpp" + + +struct Mex : Module { + static const int numSteps = 8; + enum ParamIds { + ENUMS(STEP_PARAM, numSteps), + NUM_PARAMS + }; + enum InputIds { + GATE_IN_INPUT, + NUM_INPUTS + }; + enum OutputIds { + OUT_OUTPUT, + NUM_OUTPUTS + }; + enum LightIds { + ENUMS(LED, numSteps), + NUM_LIGHTS + }; + enum StepState { + GATE_IN_MODE, + MUTE_MODE, + MUXLICER_MODE + }; + + dsp::SchmittTrigger gateInTrigger; + + struct GateSwitchParamQuantity : ParamQuantity { + std::string getDisplayValueString() override { + + switch ((StepState) ParamQuantity::getValue()) { + case GATE_IN_MODE: return "Gate in/Clock Out"; + case MUTE_MODE: return "Muted"; + case MUXLICER_MODE: return "All Gates"; + default: return ""; + } + } + }; + + Mex() { + config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); + + for (int i = 0; i < 8; ++i) { + configParam(STEP_PARAM + i, 0.f, 2.f, 0.f, string::f("Step %d", i + 1)); + } + } + + Muxlicer* findHostModulePtr(Module* module) { + if (module) { + if (module->leftExpander.module) { + // if it's Muxlicer, we're done + if (module->leftExpander.module->model == modelMuxlicer) { + return reinterpret_cast(module->leftExpander.module); + } + // if it's Mex, keep recursing + else if (module->leftExpander.module->model == modelMex) { + return findHostModulePtr(module->leftExpander.module); + } + } + } + + return nullptr; + } + + void process(const ProcessArgs& args) override { + + for (int i = 0; i < 8; i++) { + lights[i].setBrightness(0.f); + } + + Muxlicer const* mother = findHostModulePtr(this); + + if (mother) { + + float gate = 0.f; + + if (mother->playState != Muxlicer::STATE_STOPPED) { + const int currentStep = clamp(mother->addressIndex, 0, 7); + StepState state = (StepState) params[STEP_PARAM + currentStep].getValue(); + if (state == MUXLICER_MODE) { + gate = mother->isAllGatesOutHigh; + } + else if (state == GATE_IN_MODE) { + // gate in will convert non-gate signals to gates (via schmitt trigger) + // if input is present + if (inputs[GATE_IN_INPUT].isConnected()) { + gateInTrigger.process(inputs[GATE_IN_INPUT].getVoltage()); + gate = gateInTrigger.isHigh(); + } + // otherwise the main Muxlicer output clock (including divisions/multiplications) + // is normalled in + else { + gate = mother->isOutputClockHigh; + } + } + + lights[currentStep].setBrightness(gate); + } + + outputs[OUT_OUTPUT].setVoltage(gate * 10.f); + } + } +}; + + +struct MexWidget : ModuleWidget { + MexWidget(Mex* module) { + setModule(module); + setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/Mex.svg"))); + + addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); + addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + + addParam(createParamCentered(mm2px(Vec(8.088, 13.063)), module, Mex::STEP_PARAM + 0)); + addParam(createParamCentered(mm2px(Vec(8.088, 25.706)), module, Mex::STEP_PARAM + 1)); + addParam(createParamCentered(mm2px(Vec(8.088, 38.348)), module, Mex::STEP_PARAM + 2)); + addParam(createParamCentered(mm2px(Vec(8.088, 50.990)), module, Mex::STEP_PARAM + 3)); + addParam(createParamCentered(mm2px(Vec(8.088, 63.632)), module, Mex::STEP_PARAM + 4)); + addParam(createParamCentered(mm2px(Vec(8.088, 76.274)), module, Mex::STEP_PARAM + 5)); + addParam(createParamCentered(mm2px(Vec(8.088, 88.916)), module, Mex::STEP_PARAM + 6)); + addParam(createParamCentered(mm2px(Vec(8.088, 101.559)), module, Mex::STEP_PARAM + 7)); + + addInput(createInputCentered(mm2px(Vec(4.978, 113.445)), module, Mex::GATE_IN_INPUT)); + + addOutput(createOutputCentered(mm2px(Vec(15.014, 113.4)), module, Mex::OUT_OUTPUT)); + + addChild(createLightCentered>(mm2px(Vec(17.7, 13.063)), module, Mex::LED + 0)); + addChild(createLightCentered>(mm2px(Vec(17.7, 25.706)), module, Mex::LED + 1)); + addChild(createLightCentered>(mm2px(Vec(17.7, 38.348)), module, Mex::LED + 2)); + addChild(createLightCentered>(mm2px(Vec(17.7, 50.990)), module, Mex::LED + 3)); + addChild(createLightCentered>(mm2px(Vec(17.7, 63.632)), module, Mex::LED + 4)); + addChild(createLightCentered>(mm2px(Vec(17.7, 76.274)), module, Mex::LED + 5)); + addChild(createLightCentered>(mm2px(Vec(17.7, 88.916)), module, Mex::LED + 6)); + addChild(createLightCentered>(mm2px(Vec(17.7, 101.558)), module, Mex::LED + 7)); + } +}; + + +Model* modelMex = createModel("Mex"); \ No newline at end of file diff --git a/src/Morphader.cpp b/src/Morphader.cpp new file mode 100644 index 0000000..16cd1b7 --- /dev/null +++ b/src/Morphader.cpp @@ -0,0 +1,296 @@ +#include "plugin.hpp" + +using simd::float_4; + +// equal sum crossfade, -1 <= p <= 1 +template +inline T equalSumCrossfade(T a, T b, const float p) { + return a * (0.5f * (1.f - p)) + b * (0.5f * (1.f + p)); +} + +// equal power crossfade, -1 <= p <= 1 +template +inline T equalPowerCrossfade(T a, T b, const float p) { + //return std::min(std::exp(4.f * p), 1.f) * b + std::min(std::exp(4.f * -p), 1.f) * a; + return std::min(exponentialBipolar80Pade_5_4(p + 1), 1.f) * b + std::min(exponentialBipolar80Pade_5_4(1 - p), 1.f) * a; +} + +// TExponentialSlewLimiter doesn't appear to work as is required for this application. +// I think it is due to the absence of the logic that stops the output rising / falling too quickly, +// i.e. faster than the original signal? For now, we use this implementation (essentialy the same as +// SlewLimiter.cpp) +struct ExpLogSlewLimiter { + + float out = 0.f; + float slew = 0.f; + + void reset() { + out = 0.f; + } + + void setSlew(float slew) { + this->slew = slew; + } + float process(float deltaTime, float in) { + if (in > out) { + out += slew * (in - out) * deltaTime; + if (out > in) { + out = in; + } + } + else if (in < out) { + out += slew * (in - out) * deltaTime; + if (out < in) { + out = in; + } + } + return out; + } +}; + + +struct Morphader : Module { + enum ParamIds { + CV_PARAM, + ENUMS(A_LEVEL, 4), + ENUMS(B_LEVEL, 4), + ENUMS(MODE, 4), + FADER_LAG_PARAM, + FADER_PARAM, + NUM_PARAMS + }; + enum InputIds { + ENUMS(CV_INPUT, 4), + ENUMS(A_INPUT, 4), + ENUMS(B_INPUT, 4), + NUM_INPUTS + }; + enum OutputIds { + ENUMS(OUT, 4), + NUM_OUTPUTS + }; + enum LightIds { + ENUMS(A_LED, 4), + ENUMS(B_LED, 4), + NUM_LIGHTS + }; + enum CrossfadeMode { + AUDIO_MODE, + CV_MODE + }; + + static const int NUM_MIXER_CHANNELS = 4; + const float_4 normal10VSimd = {10.f}; + ExpLogSlewLimiter slewLimiter; + + // minimum and maximum slopes in volts per second, they specify the time to get + // from A (-1) to B (+1) + constexpr static float slewMin = 2.0 / 15.f; + constexpr static float slewMax = 2.0 / 0.01f; + + struct AudioCVModeParam : ParamQuantity { + std::string getDisplayValueString() override { + switch (static_cast(getValue())) { + case AUDIO_MODE: return "Audio"; + case CV_MODE: return "CV"; + default: assert(false); + } + } + }; + + Morphader() { + config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); + + configParam(CV_PARAM, 0.f, 1.f, 1.f, "CV"); + + for (int i = 0; i < NUM_MIXER_CHANNELS; i++) { + configParam(A_LEVEL + i, 0.f, 1.f, 0.f, "A level " + std::to_string(i + 1)); + } + for (int i = 0; i < NUM_MIXER_CHANNELS; i++) { + configParam(B_LEVEL + i, 0.f, 1.f, 0.f, "B level " + std::to_string(i + 1)); + } + for (int i = 0; i < NUM_MIXER_CHANNELS; i++) { + configParam(MODE + i, AUDIO_MODE, CV_MODE, AUDIO_MODE, "Mode " + std::to_string(i + 1)); + } + + configParam(FADER_LAG_PARAM, 2.0f / slewMax, 2.0f / slewMin, 2.0f / slewMax, "Fader lag", "s"); + configParam(FADER_PARAM, -1.f, 1.f, 0.f, "Fader"); + } + + // determine the cross-fade between -1 (A) and +1 (B) for each of the 4 channels + float_4 determineChannelCrossfades(const float deltaTime) { + + float_4 channelCrossfades = {}; + const float slewLambda = 2.0f / params[FADER_LAG_PARAM].getValue(); + slewLimiter.setSlew(slewLambda); + const float masterCrossfadeValue = slewLimiter.process(deltaTime, params[FADER_PARAM].getValue()); + + for (int i = 0; i < NUM_MIXER_CHANNELS; i++) { + + if (i == 0) { + // CV will be added to master for channel 1, and if not connected, the normalled value of 5.0V will correspond to the midpoint + const float crossfadeCV = clamp(inputs[CV_INPUT + i].getVoltage(), 0.f, 10.f); + channelCrossfades[i] = params[CV_PARAM].getValue() * rescale(crossfadeCV, 0.f, 10.f, 0.f, +2.f) + masterCrossfadeValue; + } + else { + // if present for the current channel, CV has total control (crossfader is ignored) + if (inputs[CV_INPUT + i].isConnected()) { + const float crossfadeCV = clamp(inputs[CV_INPUT + i].getVoltage(), 0.f, 10.f); + channelCrossfades[i] = rescale(crossfadeCV, 0.f, 10.f, -1.f, +1.f); + } + // if channel 1 is plugged in, but this channel isn't, channel 1 is normalled - in + // this scenario, however the CV is summed with the crossfader + else if (inputs[CV_INPUT + 0].isConnected()) { + const float crossfadeCV = clamp(inputs[CV_INPUT + 0].getVoltage(), 0.f, 10.f); + channelCrossfades[i] = params[CV_PARAM].getValue() * rescale(crossfadeCV, 0.f, 10.f, 0.f, +2.f) + masterCrossfadeValue; + } + else { + channelCrossfades[i] = masterCrossfadeValue; + } + } + + channelCrossfades[i] = clamp(channelCrossfades[i], -1.f, +1.f); + } + + return channelCrossfades; + } + + void process(const ProcessArgs& args) override { + + int maxChannels = 1; + float_4 mix[4] = {}; + const float_4 channelCrossfades = determineChannelCrossfades(args.sampleTime); + + for (int i = 0; i < NUM_MIXER_CHANNELS; i++) { + + const int channels = std::max(std::max(inputs[A_INPUT + i].getChannels(), inputs[B_INPUT + i].getChannels()), 1); + // keep track of the max number of channels for the mix output, noting that if channels are taken out of the mix + // (i.e. they're connected) they shouldn't contribute to the mix polyphony calculation + if (!outputs[OUT + i].isConnected()) { + maxChannels = std::max(maxChannels, channels); + } + + float_4 out[4] = {}; + for (int c = 0; c < channels; c += 4) { + float_4 inA = inputs[A_INPUT + i].getNormalVoltageSimd(normal10VSimd, c) * params[A_LEVEL + i].getValue(); + float_4 inB = inputs[B_INPUT + i].getNormalVoltageSimd(normal10VSimd, c) * params[B_LEVEL + i].getValue(); + + switch (static_cast(params[MODE + i].getValue())) { + case CV_MODE: { + out[c / 4] = equalSumCrossfade(inA, inB, channelCrossfades[i]); + break; + } + case AUDIO_MODE: { + // in audio mode, close to the centre point it is possible to get large voltages + // (e.g. if A and B are both 10V const). however according to the standard, it is + // better not to clip this https://vcvrack.com/manual/VoltageStandards#Output-Saturation + out[c / 4] = equalPowerCrossfade(inA, inB, channelCrossfades[i]); + break; + } + default: { + out[c / 4] = 0.f; + } + } + } + + // if output is patched, the channel is taken out of the mix + if (outputs[OUT + i].isConnected() && i != NUM_MIXER_CHANNELS - 1) { + outputs[OUT + i].setChannels(channels); + for (int c = 0; c < channels; c += 4) { + outputs[OUT + i].setVoltageSimd(out[c / 4], c); + } + } + else { + for (int c = 0; c < channels; c += 4) { + mix[c / 4] += out[c / 4]; + } + } + + if (i == NUM_MIXER_CHANNELS - 1) { + outputs[OUT + i].setChannels(maxChannels); + + for (int c = 0; c < maxChannels; c += 4) { + outputs[OUT + i].setVoltageSimd(mix[c / 4], c); + } + } + + switch (static_cast(params[MODE + i].getValue())) { + case AUDIO_MODE: { + lights[A_LED + i].setBrightness(equalPowerCrossfade(1.f, 0.f, channelCrossfades[i])); + lights[B_LED + i].setBrightness(equalPowerCrossfade(0.f, 1.f, channelCrossfades[i])); + break; + } + case CV_MODE: { + lights[A_LED + i].setBrightness(equalSumCrossfade(1.f, 0.f, channelCrossfades[i])); + lights[B_LED + i].setBrightness(equalSumCrossfade(0.f, 1.f, channelCrossfades[i])); + break; + } + default: { + lights[A_LED + i].setBrightness(0.f); + lights[B_LED + i].setBrightness(0.f); + break; + } + } + } // end loop over mixer channels + } +}; + + +struct MorphaderWidget : ModuleWidget { + MorphaderWidget(Morphader* module) { + setModule(module); + setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/Morphader.svg"))); + + addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); + addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); + addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + + addParam(createParamCentered(mm2px(Vec(10.817, 15.075)), module, Morphader::CV_PARAM)); + addParam(createParamCentered(mm2px(Vec(30.243, 30.537)), module, Morphader::A_LEVEL + 0)); + addParam(createParamCentered(mm2px(Vec(30.243, 48.017)), module, Morphader::A_LEVEL + 1)); + addParam(createParamCentered(mm2px(Vec(30.243, 65.523)), module, Morphader::A_LEVEL + 2)); + addParam(createParamCentered(mm2px(Vec(30.243, 83.051)), module, Morphader::A_LEVEL + 3)); + addParam(createParamCentered(mm2px(Vec(52.696, 30.537)), module, Morphader::B_LEVEL + 0)); + addParam(createParamCentered(mm2px(Vec(52.696, 48.017)), module, Morphader::B_LEVEL + 1)); + addParam(createParamCentered(mm2px(Vec(52.696, 65.523)), module, Morphader::B_LEVEL + 2)); + addParam(createParamCentered(mm2px(Vec(52.696, 83.051)), module, Morphader::B_LEVEL + 3)); + addParam(createParam(mm2px(Vec(39.775, 28.107)), module, Morphader::MODE + 0)); + addParam(createParam(mm2px(Vec(39.775, 45.627)), module, Morphader::MODE + 1)); + addParam(createParam(mm2px(Vec(39.775, 63.146)), module, Morphader::MODE + 2)); + addParam(createParam(mm2px(Vec(39.775, 80.666)), module, Morphader::MODE + 3)); + addParam(createParamCentered(mm2px(Vec(10.817, 99.242)), module, Morphader::FADER_LAG_PARAM)); + addParam(createParamCentered(mm2px(Vec(30., 114.25)), module, Morphader::FADER_PARAM)); + + addInput(createInputCentered(mm2px(Vec(25.214, 14.746)), module, Morphader::CV_INPUT + 0)); + addInput(createInputCentered(mm2px(Vec(35.213, 14.746)), module, Morphader::CV_INPUT + 1)); + addInput(createInputCentered(mm2px(Vec(45.236, 14.746)), module, Morphader::CV_INPUT + 2)); + addInput(createInputCentered(mm2px(Vec(55.212, 14.746)), module, Morphader::CV_INPUT + 3)); + addInput(createInputCentered(mm2px(Vec(5.812, 32.497)), module, Morphader::A_INPUT + 0)); + addInput(createInputCentered(mm2px(Vec(5.812, 48.017)), module, Morphader::A_INPUT + 1)); + addInput(createInputCentered(mm2px(Vec(5.812, 65.523)), module, Morphader::A_INPUT + 2)); + addInput(createInputCentered(mm2px(Vec(5.812, 81.185)), module, Morphader::A_INPUT + 3)); + addInput(createInputCentered(mm2px(Vec(15.791, 32.497)), module, Morphader::B_INPUT + 0)); + addInput(createInputCentered(mm2px(Vec(15.791, 48.017)), module, Morphader::B_INPUT + 1)); + addInput(createInputCentered(mm2px(Vec(15.791, 65.523)), module, Morphader::B_INPUT + 2)); + addInput(createInputCentered(mm2px(Vec(15.791, 81.185)), module, Morphader::B_INPUT + 3)); + + addOutput(createOutputCentered(mm2px(Vec(25.177, 100.5)), module, Morphader::OUT + 0)); + addOutput(createOutputCentered(mm2px(Vec(35.177, 100.5)), module, Morphader::OUT + 1)); + addOutput(createOutputCentered(mm2px(Vec(45.177, 100.5)), module, Morphader::OUT + 2)); + addOutput(createOutputCentered(mm2px(Vec(55.176, 100.5)), module, Morphader::OUT + 3)); + + addChild(createLightCentered>(mm2px(Vec(37.594, 24.378)), module, Morphader::A_LED + 0)); + addChild(createLightCentered>(mm2px(Vec(37.594, 41.908)), module, Morphader::A_LED + 1)); + addChild(createLightCentered>(mm2px(Vec(37.594, 59.488)), module, Morphader::A_LED + 2)); + addChild(createLightCentered>(mm2px(Vec(37.594, 76.918)), module, Morphader::A_LED + 3)); + + addChild(createLightCentered>(mm2px(Vec(45.332, 24.378)), module, Morphader::B_LED + 0)); + addChild(createLightCentered>(mm2px(Vec(45.332, 41.908)), module, Morphader::B_LED + 1)); + addChild(createLightCentered>(mm2px(Vec(45.332, 59.488)), module, Morphader::B_LED + 2)); + addChild(createLightCentered>(mm2px(Vec(45.332, 76.918)), module, Morphader::B_LED + 3)); + } +}; + + +Model* modelMorphader = createModel("Morphader"); \ No newline at end of file diff --git a/src/Muxlicer.hpp b/src/Muxlicer.hpp new file mode 100644 index 0000000..ef38195 --- /dev/null +++ b/src/Muxlicer.hpp @@ -0,0 +1,1007 @@ +#pragma once +#include "plugin.hpp" + +using simd::float_4; + +// an implementation of a performable, 3-stage switch, i.e. where +// the state triggers after being dragged a certain distance +struct BefacoSwitchMomentary : SVGSwitch { + BefacoSwitchMomentary() { + momentary = true; + addFrame(APP->window->loadSvg(asset::system("res/ComponentLibrary/BefacoSwitch_0.svg"))); + addFrame(APP->window->loadSvg(asset::system("res/ComponentLibrary/BefacoSwitch_1.svg"))); + addFrame(APP->window->loadSvg(asset::system("res/ComponentLibrary/BefacoSwitch_2.svg"))); + } + + void onDragStart(const event::DragStart& e) override { + latched = false; + startMouseY = APP->scene->rack->mousePos.y; + ParamWidget::onDragStart(e); + } + + void onDragMove(const event::DragMove& e) override { + + float diff = APP->scene->rack->mousePos.y - startMouseY; + + // Once the user has dragged the mouse a "threshold" distance, latch + // to disallow further changes of state until the mouse is released. + // We don't just setValue(1) (default/rest state) because this creates a + // jarring UI experience + if (diff < -10 && !latched) { + paramQuantity->setValue(2); + latched = true; + } + if (diff > 10 && !latched) { + paramQuantity->setValue(0); + latched = true; + } + + ParamWidget::onDragMove(e); + } + + void onDragEnd(const event::DragEnd& e) override { + // on release, the switch resets to default/neutral/middle position + paramQuantity->setValue(1); + latched = false; + ParamWidget::onDragEnd(e); + } + + // do nothing + void randomize() override {} + + float startMouseY = 0.f; + bool latched = false; +}; + + +// Class which can yield a divided clock state, specifically where the +// gate is generated at request time through getGate(), rather than during +// process() - this means that different divisions of clock can be requested +// at any point in time. In contrast, the division/multiplication setting for +// ClockMultDiv cannot easily be changed _during_ a clock tick. +struct MultiGateClock { + + float remaining = 0.f; + float fullPulseLength = 0.f; + + /** Immediately disables the pulse */ + void reset(float newfullPulseLength) { + fullPulseLength = newfullPulseLength; + remaining = fullPulseLength; + } + + /** Advances the state by `deltaTime`. Returns whether the pulse is in the HIGH state. */ + bool process(float deltaTime) { + if (remaining > 0.f) { + remaining -= deltaTime; + return true; + } + return false; + } + + bool getGate(int gateMode) { + + if (gateMode == 0) { + // always on (special case) + return true; + } + else if (gateMode < 0 || remaining <= 0) { + // disabled (or elapsed) + return false; + } + + const float multiGateOnLength = fullPulseLength / ((gateMode > 0) ? (2.f * gateMode) : 1.0f); + const bool isOddPulse = int(floor(remaining / multiGateOnLength)) % 2; + + return isOddPulse; + } +}; + + +// Class for generating a clock sequence after setting a clock multiplication or division, +// given a stream of clock pulses as the "base" clock. +// Implementation is heavily inspired by BogAudio RGate, with modification +struct MultDivClock { + + // convention: negative values are used for division (1/mult), positive for multiplication (x mult) + // multDiv = 0 should not be used, but if it is it will result in no modification to the clock + int multDiv = 1; + float secondsSinceLastClock = -1.0f; + float inputClockLengthSeconds = -1.0f; + + // count how many divisions we've had + int dividerCount = 0; + + float dividedProgressSeconds = 0.f; + + // returns the gated clock signal, returns true when high + bool process(float deltaTime, bool clockPulseReceived) { + + if (clockPulseReceived) { + // update our record of the incoming clock spacing + if (secondsSinceLastClock > 0.0f) { + inputClockLengthSeconds = secondsSinceLastClock; + } + secondsSinceLastClock = 0.0f; + } + + bool out = false; + if (secondsSinceLastClock >= 0.0f) { + secondsSinceLastClock += deltaTime; + + // negative values are used for division (x 1/mult), positive for multiplication (x mult) + const int division = std::max(-multDiv, 1); + const int multiplication = std::max(multDiv, 1); + + if (clockPulseReceived) { + if (dividerCount < 1) { + dividedProgressSeconds = 0.0f; + } + else { + dividedProgressSeconds += deltaTime; + } + ++dividerCount; + if (dividerCount >= division) { + dividerCount = 0; + } + } + else { + dividedProgressSeconds += deltaTime; + } + + // lengths of the mult/div versions of the clock + const float dividedSeconds = inputClockLengthSeconds * (float) division; + const float multipliedSeconds = dividedSeconds / (float) multiplication; + + // length of the output gate (s) + const float gateSeconds = std::max(0.001f, multipliedSeconds * 0.5f); + + if (dividedProgressSeconds < dividedSeconds) { + float multipliedProgressSeconds = dividedProgressSeconds / multipliedSeconds; + multipliedProgressSeconds -= (float)(int)multipliedProgressSeconds; + multipliedProgressSeconds *= multipliedSeconds; + out = (multipliedProgressSeconds <= gateSeconds); + } + } + return out; + } + + float getEffectiveClockLength() { + // negative values are used for division (x 1/mult), positive for multiplication (x mult) + const int division = std::max(-multDiv, 1); + const int multiplication = std::max(multDiv, 1); + + // lengths of the mult/div versions of the clock + const float dividedSeconds = inputClockLengthSeconds * (float) division; + const float multipliedSeconds = dividedSeconds / (float) multiplication; + + return multipliedSeconds; + } +}; + +static const std::vector clockOptionsQuadratic = {-16, -8, -4, -2, 1, 2, 4, 8, 16}; +static const std::vector clockOptionsAll = {-16, -15, -14, -13, -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, 1, + 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 + }; + +inline std::string getClockOptionString(const int clockOption) { + return (clockOption < 0) ? ("x 1/" + std::to_string(-clockOption)) : ("x " + std::to_string(clockOption)); +} + +struct Muxlicer : Module { + enum ParamIds { + PLAY_PARAM, + ADDRESS_PARAM, + GATE_MODE_PARAM, + DIV_MULT_PARAM, + ENUMS(LEVEL_PARAMS, 8), + NUM_PARAMS + }; + enum InputIds { + GATE_MODE_INPUT, + ADDRESS_INPUT, + CLOCK_INPUT, + RESET_INPUT, + COM_INPUT, + ENUMS(MUX_INPUTS, 8), + ALL_INPUT, + NUM_INPUTS + }; + enum OutputIds { + CLOCK_OUTPUT, + ALL_GATES_OUTPUT, + EOC_OUTPUT, + ENUMS(GATE_OUTPUTS, 8), + ENUMS(MUX_OUTPUTS, 8), + COM_OUTPUT, + ALL_OUTPUT, + NUM_OUTPUTS + }; + enum LightIds { + CLOCK_LIGHT, + ENUMS(GATE_LIGHTS, 8), + NUM_LIGHTS + }; + + enum ModeCOMIO { + COM_1_IN_8_OUT, + COM_8_IN_1_OUT + }; + + enum PlayState { + STATE_PLAY_ONCE, + STATE_STOPPED, + STATE_PLAY + }; + + /* + This shows how the values of the gate mode knob + CV map onto gate triggers. + See also getGateMode() + value | description | quadratic only mode + -1 no gate | ✔ + 0 gate (full timestep) | x + +1 half timestep | ✔ + 2 two gates | ✔ + 3 three gates | x + 4 four gates | ✔ + 5 five gates | x + 6 six gates | x + 7 seven gates | x + 8 eight gates | ✔ + */ + const int possibleQuadraticGates[5] = {-1, 1, 2, 4, 8}; + bool quadraticGatesOnly = false; + bool outputClockFollowsPlayMode = false; + + PlayState playState = STATE_STOPPED; + dsp::BooleanTrigger playStateTrigger; + + uint32_t runIndex; // which step are we on (0 to 7) + uint32_t addressIndex = 0; + bool playHeadHasReset = false; + bool tapped = false; + + // used to track the clock (e.g. if external clock is not connected). NOTE: this clock + // is defined _prior_ to any clock division/multiplication logic + float internalClockProgress = 0.f; + float internalClockLength = 0.25f; + + float tapTime = 99999; // used to track the time between clock pulses (or taps?) + dsp::SchmittTrigger inputClockTrigger; // to detect incoming clock pulses + dsp::BooleanTrigger mainClockTrigger; // to detect when divided/multiplied version of the clock signal has rising edge + dsp::SchmittTrigger resetTrigger; // to detect the reset signal + dsp::PulseGenerator resetTimer; // leave a grace period before advancing the step + dsp::PulseGenerator endOfCyclePulse; // fire a signal at the end of cycle + dsp::BooleanTrigger tapTempoTrigger; // to only trigger tap tempo when push is first detected + MultDivClock mainClockMultDiv; // to produce a divided/multiplied version of the (internal or external) clock signal + MultDivClock outputClockMultDiv; // to produce a divided/multiplied version of the output clock signal + MultiGateClock multiClock; // to easily produce a divided version of the main clock (where division can be changed at any point) + bool usingExternalClock = false; // is there a clock plugged into clock in (external) or not (internal) + + const static int SEQUENCE_LENGTH = 8; + ModeCOMIO modeCOMIO = COM_8_IN_1_OUT; // are we in 1-in-8-out mode, or 8-in-1-out mode + int allInNormalVoltage = 10; // what voltage is normalled into the "All In" input, selectable via context menu + Module* rightModule; // for the expander + + // these are class variables, rather than scoped to process(...), to allow expanders to read + // all gate output and clock output + bool isAllGatesOutHigh = false; + bool isOutputClockHigh = false; + + struct DivMultKnobParamQuantity : ParamQuantity { + std::string getDisplayValueString() override { + Muxlicer* moduleMuxlicer = reinterpret_cast(module); + if (moduleMuxlicer != nullptr) { + return getClockOptionString(moduleMuxlicer->getClockOptionFromParam()); + } + else { + return ""; + } + } + }; + + struct GateModeParamQuantity : ParamQuantity { + std::string getDisplayValueString() override { + Muxlicer* moduleMuxlicer = reinterpret_cast(module); + + if (moduleMuxlicer != nullptr) { + bool attenuatorMode = moduleMuxlicer->inputs[GATE_MODE_INPUT].isConnected(); + if (attenuatorMode) { + return ParamQuantity::getDisplayValueString(); + } + else { + const int gate = moduleMuxlicer->getGateMode(); + if (gate < 0) { + return "No gate"; + } + else if (gate == 0) { + return "1/2 gate"; + } + else { + return string::f("%d gate(s)", gate); + } + } + } + else { + return ParamQuantity::getDisplayValueString(); + } + } + }; + + // given param (in range 0 to 1), return the clock option from an array of choices + int getClockOptionFromParam() { + if (quadraticGatesOnly) { + const int clockOptionIndex = round(params[Muxlicer::DIV_MULT_PARAM].getValue() * (clockOptionsQuadratic.size() - 1)); + return clockOptionsQuadratic[clockOptionIndex]; + } + else { + const int clockOptionIndex = round(params[Muxlicer::DIV_MULT_PARAM].getValue() * (clockOptionsAll.size() - 1)); + return clockOptionsAll[clockOptionIndex]; + } + } + + // given a the mult/div setting for the main clock, find the index of this from an array of valid choices, + // and convert to a value between 0 and 1 (update the DIV_MULT_PARAM param) + void updateParamFromMainClockMultDiv() { + + auto const& arrayToSearch = quadraticGatesOnly ? clockOptionsQuadratic : clockOptionsAll; + auto const it = std::find(arrayToSearch.begin(), arrayToSearch.end(), mainClockMultDiv.multDiv); + + // try to find the index in the array of valid clock mults/divs + if (it != arrayToSearch.end()) { + int index = it - arrayToSearch.begin(); + float paramIndex = (float) index / (arrayToSearch.size() - 1); + params[Muxlicer::DIV_MULT_PARAM].setValue(paramIndex); + } + // if not, default to 0.5 (which should correspond to x1, no mult/div) + else { + params[Muxlicer::DIV_MULT_PARAM].setValue(0.5); + } + } + + Muxlicer() { + config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); + configParam(Muxlicer::PLAY_PARAM, STATE_PLAY_ONCE, STATE_PLAY, STATE_STOPPED, "Play switch"); + configParam(Muxlicer::ADDRESS_PARAM, -1.f, 7.f, -1.f, "Address"); + configParam(Muxlicer::GATE_MODE_PARAM, -1.f, 8.f, 1.f, "Gate mode"); + configParam(Muxlicer::DIV_MULT_PARAM, 0, 1, 0.5, "Main clock mult/div"); + + for (int i = 0; i < SEQUENCE_LENGTH; ++i) { + configParam(Muxlicer::LEVEL_PARAMS + i, 0.0, 1.0, 1.0, string::f("Gain %d", i + 1)); + } + + onReset(); + } + + void onReset() override { + internalClockLength = 0.250f; + internalClockProgress = 0; + runIndex = 0; + mainClockMultDiv.multDiv = 1; + outputClockMultDiv.multDiv = 1; + quadraticGatesOnly = false; + playState = STATE_STOPPED; + } + + void process(const ProcessArgs& args) override { + + usingExternalClock = inputs[CLOCK_INPUT].isConnected(); + + bool externalClockPulseReceived = false; + // a clock pulse does two things: 1) sets the internal clock (based on timing between two pulses), which + // would continue were the clock input to be removed, and 2) synchronises/drives the clock (if clock input present) + if (usingExternalClock && inputClockTrigger.process(rescale(inputs[CLOCK_INPUT].getVoltage(), 0.1f, 2.f, 0.f, 1.f))) { + externalClockPulseReceived = true; + } + // this can also be sent by tap tempo + else if (!usingExternalClock && tapTempoTrigger.process(tapped)) { + externalClockPulseReceived = true; + tapped = false; + } + + mainClockMultDiv.multDiv = getClockOptionFromParam(); + + processPlayResetLogic(); + + const float address = params[ADDRESS_PARAM].getValue() + inputs[ADDRESS_INPUT].getVoltage(); + const bool isAddressInRunMode = address < 0.f; + + // even if we have an external clock, use its pulses to time/sync the internal clock + // so that it will remain running even after CLOCK_INPUT is disconnected + if (externalClockPulseReceived) { + // track length between received clock pulses (using external clock) or taps + // of the tap-tempo menu item (if sufficiently short) + if (usingExternalClock || tapTime < 2.f) { + internalClockLength = tapTime; + } + tapTime = 0; + internalClockProgress = 0; + } + tapTime += args.sampleTime; + internalClockProgress += args.sampleTime; + + // track if the internal clock has "ticked" + const bool internalClockPulseReceived = (internalClockProgress >= internalClockLength); + if (internalClockPulseReceived) { + internalClockProgress = 0.f; + } + + // we can be in one of two clock modes: + // * external (decided by pulses to CLOCK_INPUT) + // * internal (decided by internalClockProgress exceeding the internal clock length) + // + // choose which clock source we are to use + const bool clockPulseReceived = usingExternalClock ? externalClockPulseReceived : internalClockPulseReceived; + // apply the main clock div/mult logic to whatever clock source we're using - mainClockMultDiv outputs a gate sequence + // so we must use a BooleanTrigger on the divided/mult'd signal in order to detect rising edge / when to advance the sequence + const bool dividedMultipliedClockPulseReceived = mainClockTrigger.process(mainClockMultDiv.process(args.sampleTime, clockPulseReceived)); + + // see https://vcvrack.com/manual/VoltageStandards#Timing + const bool resetGracePeriodActive = resetTimer.process(args.sampleTime); + + if (dividedMultipliedClockPulseReceived) { + if (isAddressInRunMode && !resetGracePeriodActive && playState != STATE_STOPPED) { + runIndex++; + if (runIndex >= SEQUENCE_LENGTH) { + + // the sequence resets by placing the play head at the final step (so that the next clock pulse + // ticks over onto the first step) - so if we are on the final step _because_ we've reset, + // then don't fire EOC + if (playHeadHasReset) { + playHeadHasReset = false; + runIndex = 0; + } + // otherwise we've naturally arrived at the last step so do fire EOC etc + else { + endOfCyclePulse.trigger(1e-3); + + // stop on this step if in one shot mode + if (playState == STATE_PLAY_ONCE) { + playState = STATE_STOPPED; + } + else { + runIndex = 0; + } + } + } + } + multiClock.reset(mainClockMultDiv.getEffectiveClockLength()); + + if (isAddressInRunMode) { + addressIndex = runIndex; + } + else { + addressIndex = clamp((int) roundf(address), 0, SEQUENCE_LENGTH - 1); + } + + for (int i = 0; i < 8; i++) { + outputs[GATE_OUTPUTS + i].setVoltage(0.f); + } + } + + // Gates + for (int i = 0; i < 8; i++) { + outputs[GATE_OUTPUTS + i].setVoltage(0.f); + lights[GATE_LIGHTS + i].setBrightness(0.f); + } + outputs[ALL_GATES_OUTPUT].setVoltage(0.f); + + multiClock.process(args.sampleTime); + const int gateMode = getGateMode(); + + // current gate output _and_ "All Gates" output both get the gate pattern from multiClock + // NOTE: isAllGatesOutHigh can also be read by expanders + isAllGatesOutHigh = multiClock.getGate(gateMode) && (playState != STATE_STOPPED); + outputs[GATE_OUTPUTS + addressIndex].setVoltage(isAllGatesOutHigh * 10.f); + lights[GATE_LIGHTS + addressIndex].setBrightness(isAllGatesOutHigh * 1.f); + outputs[ALL_GATES_OUTPUT].setVoltage(isAllGatesOutHigh * 10.f); + + if (modeCOMIO == COM_1_IN_8_OUT) { + const int numActiveEngines = std::max(inputs[ALL_INPUT].getChannels(), inputs[COM_INPUT].getChannels()); + const float stepVolume = params[LEVEL_PARAMS + addressIndex].getValue(); + + for (int c = 0; c < numActiveEngines; c += 4) { + // Mux outputs (all zero, except active step, if playing) + for (int i = 0; i < 8; i++) { + outputs[MUX_OUTPUTS + i].setVoltageSimd(0.f, c); + } + + const float_4 com_input = inputs[COM_INPUT].getPolyVoltageSimd(c); + if (outputs[MUX_OUTPUTS + addressIndex].isConnected()) { + outputs[MUX_OUTPUTS + addressIndex].setVoltageSimd(stepVolume * com_input, c); + outputs[ALL_OUTPUT].setVoltageSimd(0.f, c); + } + else if (outputs[ALL_OUTPUT].isConnected()) { + outputs[ALL_OUTPUT].setVoltageSimd(stepVolume * com_input, c); + } + } + + for (int i = 0; i < 8; i++) { + outputs[MUX_OUTPUTS + i].setChannels(numActiveEngines); + } + outputs[ALL_OUTPUT].setChannels(numActiveEngines); + } + else if (modeCOMIO == COM_8_IN_1_OUT) { + // we need at least one active engine, even if nothing is connected + // as we want the voltage that is normalled to All In to be processed + int numActiveEngines = std::max(1, inputs[ALL_INPUT].getChannels()); + for (int i = 0; i < 8; i++) { + numActiveEngines = std::max(numActiveEngines, inputs[MUX_INPUTS + i].getChannels()); + } + + const float stepVolume = params[LEVEL_PARAMS + addressIndex].getValue(); + for (int c = 0; c < numActiveEngines; c += 4) { + const float_4 allInValue = inputs[ALL_INPUT].getNormalPolyVoltageSimd((float_4) allInNormalVoltage, c); + const float_4 stepValue = inputs[MUX_INPUTS + addressIndex].getNormalPolyVoltageSimd(allInValue, c) * stepVolume; + outputs[COM_OUTPUT].setVoltageSimd(stepValue, c); + } + outputs[COM_OUTPUT].setChannels(numActiveEngines); + } + + // there is an option to stop output clock when play stops + const bool playStateMask = !outputClockFollowsPlayMode || (playState != STATE_STOPPED); + // NOTE: outputClockOut can also be read by expanders + isOutputClockHigh = outputClockMultDiv.process(args.sampleTime, clockPulseReceived) && playStateMask; + outputs[CLOCK_OUTPUT].setVoltage(isOutputClockHigh * 10.f); + lights[CLOCK_LIGHT].setBrightness(isOutputClockHigh * 1.f); + + // end of cycle trigger trigger + outputs[EOC_OUTPUT].setVoltage(endOfCyclePulse.process(args.sampleTime) ? 10.f : 0.f); + + } + + void processPlayResetLogic() { + + const bool resetPulseInReceived = resetTrigger.process(rescale(inputs[RESET_INPUT].getVoltage(), 0.1f, 2.f, 0.f, 1.f)); + if (resetPulseInReceived) { + playHeadHasReset = true; + runIndex = 8; + + if (playState == STATE_STOPPED) { + playState = STATE_PLAY_ONCE; + } + resetTimer.trigger(); + } + + // if the play switch has effectively been activated for the first time, + // i.e. it's not just still being held + const bool switchIsActive = params[PLAY_PARAM].getValue() != STATE_STOPPED; + if (playStateTrigger.process(switchIsActive) && switchIsActive) { + + // if we were stopped, check for activation (normal or one-shot) + if (playState == STATE_STOPPED) { + if (params[PLAY_PARAM].getValue() == STATE_PLAY) { + playState = STATE_PLAY; + } + else if (params[PLAY_PARAM].getValue() == STATE_PLAY_ONCE) { + playState = STATE_PLAY_ONCE; + runIndex = 8; + playHeadHasReset = true; + } + } + // otherwise we are in play mode (and we've not just held onto the play switch), + // so check for stop or reset + else { + + // top switch will stop + if (params[PLAY_PARAM].getValue() == STATE_PLAY) { + playState = STATE_STOPPED; + } + // bottom will reset + else if (params[PLAY_PARAM].getValue() == STATE_PLAY_ONCE) { + playHeadHasReset = true; + runIndex = 8; + } + } + } + } + + + // determines how many gates to yield per step + int getGateMode() { + + int gate; + + if (inputs[GATE_MODE_INPUT].isConnected()) { + // with gate acting as attenuator, hardware reacts in 1V increments, + // where x V -> (x + 1) V yields (x - 1) gates in that time + float gateCV = clamp(inputs[GATE_MODE_INPUT].getVoltage(), 0.f, 10.f); + float knobAttenuation = rescale(params[GATE_MODE_PARAM].getValue(), -1.f, 8.f, 0.f, 1.f); + + gate = int (floor(gateCV * knobAttenuation)) - 1; + } + else { + gate = (int) roundf(params[GATE_MODE_PARAM].getValue()); + } + + // should be respected, but make sure + gate = clamp(gate, -1, 8); + + if (quadraticGatesOnly) { + int quadraticGateIndex = int(floor(rescale(gate, -1.f, 8.f, 0.f, 4.99f))); + return possibleQuadraticGates[clamp(quadraticGateIndex, 0, 4)]; + } + else { + return gate; + } + } + + json_t* dataToJson() override { + json_t* rootJ = json_object(); + json_object_set_new(rootJ, "modeCOMIO", json_integer(modeCOMIO)); + json_object_set_new(rootJ, "quadraticGatesOnly", json_boolean(quadraticGatesOnly)); + json_object_set_new(rootJ, "allInNormalVoltage", json_integer(allInNormalVoltage)); + json_object_set_new(rootJ, "mainClockMultDiv", json_integer(mainClockMultDiv.multDiv)); + json_object_set_new(rootJ, "outputClockMultDiv", json_integer(outputClockMultDiv.multDiv)); + json_object_set_new(rootJ, "playState", json_integer(playState)); + json_object_set_new(rootJ, "outputClockFollowsPlayMode", json_boolean(outputClockFollowsPlayMode)); + + return rootJ; + } + + void dataFromJson(json_t* rootJ) override { + json_t* modeJ = json_object_get(rootJ, "modeCOMIO"); + if (modeJ) { + modeCOMIO = (Muxlicer::ModeCOMIO) json_integer_value(modeJ); + } + + json_t* quadraticJ = json_object_get(rootJ, "quadraticGatesOnly"); + if (quadraticJ) { + quadraticGatesOnly = json_boolean_value(quadraticJ); + } + + json_t* allInNormalVoltageJ = json_object_get(rootJ, "allInNormalVoltage"); + if (allInNormalVoltageJ) { + allInNormalVoltage = json_integer_value(allInNormalVoltageJ); + } + + json_t* mainClockMultDivJ = json_object_get(rootJ, "mainClockMultDiv"); + if (mainClockMultDivJ) { + mainClockMultDiv.multDiv = json_integer_value(mainClockMultDivJ); + } + + json_t* outputClockMultDivJ = json_object_get(rootJ, "outputClockMultDiv"); + if (outputClockMultDivJ) { + outputClockMultDiv.multDiv = json_integer_value(outputClockMultDivJ); + } + + json_t* playStateJ = json_object_get(rootJ, "playState"); + if (playStateJ) { + playState = (PlayState) json_integer_value(playStateJ); + } + + json_t* outputClockFollowsPlayModeJ = json_object_get(rootJ, "outputClockFollowsPlayMode"); + if (outputClockFollowsPlayModeJ) { + outputClockFollowsPlayMode = json_boolean_value(outputClockFollowsPlayModeJ); + } + + updateParamFromMainClockMultDiv(); + } + +}; + + + +struct MuxlicerWidget : ModuleWidget { + MuxlicerWidget(Muxlicer* module) { + + setModule(module); + setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/Muxlicer.svg"))); + + addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); + addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); + addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + + addParam(createParam(mm2px(Vec(35.72963, 10.008)), module, Muxlicer::PLAY_PARAM)); + addParam(createParam(mm2px(Vec(3.84112, 10.90256)), module, Muxlicer::ADDRESS_PARAM)); + addParam(createParam(mm2px(Vec(67.83258, 10.86635)), module, Muxlicer::GATE_MODE_PARAM)); + addParam(createParam(mm2px(Vec(28.12238, 24.62151)), module, Muxlicer::DIV_MULT_PARAM)); + + addParam(createParam(mm2px(Vec(2.32728, 40.67102)), module, Muxlicer::LEVEL_PARAMS + 0)); + addParam(createParam(mm2px(Vec(12.45595, 40.67102)), module, Muxlicer::LEVEL_PARAMS + 1)); + addParam(createParam(mm2px(Vec(22.58462, 40.67102)), module, Muxlicer::LEVEL_PARAMS + 2)); + addParam(createParam(mm2px(Vec(32.7133, 40.67102)), module, Muxlicer::LEVEL_PARAMS + 3)); + addParam(createParam(mm2px(Vec(42.74195, 40.67102)), module, Muxlicer::LEVEL_PARAMS + 4)); + addParam(createParam(mm2px(Vec(52.97062, 40.67102)), module, Muxlicer::LEVEL_PARAMS + 5)); + addParam(createParam(mm2px(Vec(63.0993, 40.67102)), module, Muxlicer::LEVEL_PARAMS + 6)); + addParam(createParam(mm2px(Vec(73.22797, 40.67102)), module, Muxlicer::LEVEL_PARAMS + 7)); + + addInput(createInput(mm2px(Vec(51.568, 11.20189)), module, Muxlicer::GATE_MODE_INPUT)); + addInput(createInput(mm2px(Vec(21.13974, 11.23714)), module, Muxlicer::ADDRESS_INPUT)); + addInput(createInput(mm2px(Vec(44.24461, 24.93662)), module, Muxlicer::CLOCK_INPUT)); + addInput(createInput(mm2px(Vec(12.62135, 24.95776)), module, Muxlicer::RESET_INPUT)); + addInput(createInput(mm2px(Vec(36.3142, 98.07911)), module, Muxlicer::COM_INPUT)); + addInput(createInput(mm2px(Vec(0.895950, 109.27901)), module, Muxlicer::MUX_INPUTS + 0)); + addInput(createInput(mm2px(Vec(11.05332, 109.29256)), module, Muxlicer::MUX_INPUTS + 1)); + addInput(createInput(mm2px(Vec(21.18201, 109.29256)), module, Muxlicer::MUX_INPUTS + 2)); + addInput(createInput(mm2px(Vec(31.27625, 109.27142)), module, Muxlicer::MUX_INPUTS + 3)); + addInput(createInput(mm2px(Vec(41.40493, 109.27142)), module, Muxlicer::MUX_INPUTS + 4)); + addInput(createInput(mm2px(Vec(51.53360, 109.27142)), module, Muxlicer::MUX_INPUTS + 5)); + addInput(createInput(mm2px(Vec(61.69671, 109.29256)), module, Muxlicer::MUX_INPUTS + 6)); + addInput(createInput(mm2px(Vec(71.82537, 109.29256)), module, Muxlicer::MUX_INPUTS + 7)); + addInput(createInput(mm2px(Vec(16.11766, 98.09121)), module, Muxlicer::ALL_INPUT)); + + addOutput(createOutput(mm2px(Vec(59.8492, 24.95776)), module, Muxlicer::CLOCK_OUTPUT)); + addOutput(createOutput(mm2px(Vec(56.59663, 98.06252)), module, Muxlicer::ALL_GATES_OUTPUT)); + addOutput(createOutput(mm2px(Vec(66.72661, 98.07008)), module, Muxlicer::EOC_OUTPUT)); + addOutput(createOutput(mm2px(Vec(0.89595, 86.78581)), module, Muxlicer::GATE_OUTPUTS + 0)); + addOutput(createOutput(mm2px(Vec(11.02463, 86.77068)), module, Muxlicer::GATE_OUTPUTS + 1)); + addOutput(createOutput(mm2px(Vec(21.14758, 86.77824)), module, Muxlicer::GATE_OUTPUTS + 2)); + addOutput(createOutput(mm2px(Vec(31.27625, 86.77824)), module, Muxlicer::GATE_OUTPUTS + 3)); + addOutput(createOutput(mm2px(Vec(41.40493, 86.77824)), module, Muxlicer::GATE_OUTPUTS + 4)); + addOutput(createOutput(mm2px(Vec(51.56803, 86.79938)), module, Muxlicer::GATE_OUTPUTS + 5)); + addOutput(createOutput(mm2px(Vec(61.69671, 86.79938)), module, Muxlicer::GATE_OUTPUTS + 6)); + addOutput(createOutput(mm2px(Vec(71.79094, 86.77824)), module, Muxlicer::GATE_OUTPUTS + 7)); + + // these blocks are exclusive (for visibility / interactivity) and allows IO and OI within one module + addOutput(createOutput(mm2px(Vec(0.895950, 109.27901)), module, Muxlicer::MUX_OUTPUTS + 0)); + addOutput(createOutput(mm2px(Vec(11.05332, 109.29256)), module, Muxlicer::MUX_OUTPUTS + 1)); + addOutput(createOutput(mm2px(Vec(21.18201, 109.29256)), module, Muxlicer::MUX_OUTPUTS + 2)); + addOutput(createOutput(mm2px(Vec(31.27625, 109.27142)), module, Muxlicer::MUX_OUTPUTS + 3)); + addOutput(createOutput(mm2px(Vec(41.40493, 109.27142)), module, Muxlicer::MUX_OUTPUTS + 4)); + addOutput(createOutput(mm2px(Vec(51.53360, 109.27142)), module, Muxlicer::MUX_OUTPUTS + 5)); + addOutput(createOutput(mm2px(Vec(61.69671, 109.29256)), module, Muxlicer::MUX_OUTPUTS + 6)); + addOutput(createOutput(mm2px(Vec(71.82537, 109.29256)), module, Muxlicer::MUX_OUTPUTS + 7)); + addOutput(createOutput(mm2px(Vec(36.3142, 98.07911)), module, Muxlicer::COM_OUTPUT)); + addOutput(createOutput(mm2px(Vec(16.11766, 98.09121)), module, Muxlicer::ALL_OUTPUT)); + + updatePortVisibilityForIOMode(Muxlicer::COM_8_IN_1_OUT); + + addChild(createLight>(mm2px(Vec(71.28361, 28.02644)), module, Muxlicer::CLOCK_LIGHT)); + addChild(createLight>(mm2px(Vec(3.99336, 81.86801)), module, Muxlicer::GATE_LIGHTS + 0)); + addChild(createLight>(mm2px(Vec(14.09146, 81.86801)), module, Muxlicer::GATE_LIGHTS + 1)); + addChild(createLight>(mm2px(Vec(24.22525, 81.86801)), module, Muxlicer::GATE_LIGHTS + 2)); + addChild(createLight>(mm2px(Vec(34.35901, 81.86801)), module, Muxlicer::GATE_LIGHTS + 3)); + addChild(createLight>(mm2px(Vec(44.49277, 81.86801)), module, Muxlicer::GATE_LIGHTS + 4)); + addChild(createLight>(mm2px(Vec(54.62652, 81.86801)), module, Muxlicer::GATE_LIGHTS + 5)); + addChild(createLight>(mm2px(Vec(64.76028, 81.86801)), module, Muxlicer::GATE_LIGHTS + 6)); + addChild(createLight>(mm2px(Vec(74.89404, 81.86801)), module, Muxlicer::GATE_LIGHTS + 7)); + } + + void draw(const DrawArgs& args) override { + Muxlicer* module = dynamic_cast(this->module); + + if (module != nullptr) { + updatePortVisibilityForIOMode(module->modeCOMIO); + } + else { + // module can be null, e.g. if populating the module browser with screenshots, + // in which case just assume the default (8 in, 1 out) + updatePortVisibilityForIOMode(Muxlicer::COM_8_IN_1_OUT); + } + + ModuleWidget::draw(args); + } + + struct IOMenuItem : MenuItem { + Muxlicer* module; + MuxlicerWidget* widget; + void onAction(const event::Action& e) override { + module->modeCOMIO = Muxlicer::COM_1_IN_8_OUT; + widget->updatePortVisibilityForIOMode(module->modeCOMIO); + widget->clearCables(); + } + }; + struct OIMenuItem : MenuItem { + Muxlicer* module; + MuxlicerWidget* widget; + void onAction(const event::Action& e) override { + module->modeCOMIO = Muxlicer::COM_8_IN_1_OUT; + widget->updatePortVisibilityForIOMode(module->modeCOMIO); + widget->clearCables(); + } + }; + + struct OutputRangeChildItem : MenuItem { + Muxlicer* module; + int allInNormalVoltage; + void onAction(const event::Action& e) override { + module->allInNormalVoltage = allInNormalVoltage; + } + }; + + struct OutputRangeItem : MenuItem { + Muxlicer* module; + + Menu* createChildMenu() override { + Menu* menu = new Menu; + + std::vector voltageOptions = {1, 5, 10}; + for (auto voltageOption : voltageOptions) { + OutputRangeChildItem* rangeItem = createMenuItem(std::to_string(voltageOption) + "V", + CHECKMARK(module->allInNormalVoltage == voltageOption)); + rangeItem->allInNormalVoltage = voltageOption; + rangeItem->module = module; + menu->addChild(rangeItem); + } + + return menu; + } + }; + + struct OutputClockScalingItem : MenuItem { + Muxlicer* module; + + struct OutputClockScalingChildItem : MenuItem { + Muxlicer* module; + int clockOutMulDiv; + void onAction(const event::Action& e) override { + module->outputClockMultDiv.multDiv = clockOutMulDiv; + } + }; + + Menu* createChildMenu() override { + Menu* menu = new Menu; + + for (int clockOption : module->quadraticGatesOnly ? clockOptionsQuadratic : clockOptionsAll) { + std::string optionString = getClockOptionString(clockOption); + OutputClockScalingChildItem* clockItem = createMenuItem(optionString, + CHECKMARK(module->outputClockMultDiv.multDiv == clockOption)); + clockItem->clockOutMulDiv = clockOption; + clockItem->module = module; + menu->addChild(clockItem); + } + + return menu; + } + }; + + struct MainClockScalingItem : MenuItem { + Muxlicer* module; + + struct MainClockScalingChildItem : MenuItem { + Muxlicer* module; + int mainClockMulDiv, mainClockMulDivIndex; + + void onAction(const event::Action& e) override { + module->mainClockMultDiv.multDiv = mainClockMulDiv; + module->updateParamFromMainClockMultDiv(); + } + }; + + Menu* createChildMenu() override { + Menu* menu = new Menu; + + int i = 0; + + for (int clockOption : module->quadraticGatesOnly ? clockOptionsQuadratic : clockOptionsAll) { + std::string optionString = getClockOptionString(clockOption); + MainClockScalingChildItem* clockItem = createMenuItem(optionString, + CHECKMARK(module->mainClockMultDiv.multDiv == clockOption)); + + clockItem->mainClockMulDiv = clockOption; + clockItem->mainClockMulDivIndex = i; + clockItem->module = module; + menu->addChild(clockItem); + ++i; + } + + return menu; + } + }; + + struct QuadraticGatesMenuItem : MenuItem { + Muxlicer* module; + void onAction(const event::Action& e) override { + module->quadraticGatesOnly ^= true; + module->updateParamFromMainClockMultDiv(); + } + }; + + struct OutputClockStopStartItem : MenuItem { + Muxlicer* module; + void onAction(const event::Action& e) override { + module->outputClockFollowsPlayMode ^= true; + module->updateParamFromMainClockMultDiv(); + } + }; + + struct TapTempoItem : MenuItem { + Muxlicer* module; + void onAction(const event::Action& e) override { + module->tapped = true; + e.consume(NULL); + } + }; + + void appendContextMenu(Menu* menu) override { + Muxlicer* module = dynamic_cast(this->module); + assert(module); + + menu->addChild(new MenuSeparator()); + menu->addChild(createMenuLabel("Clock Multiplication/Division")); + + if (module->usingExternalClock) { + menu->addChild(createMenuLabel("Using external clock")); + + } + else { + TapTempoItem* tapTempoItem = createMenuItem("Tap to set internal tempo..."); + tapTempoItem->module = module; + menu->addChild(tapTempoItem); + } + + + MainClockScalingItem* mainClockScaleItem = createMenuItem("Input clock", "▸"); + mainClockScaleItem->module = module; + menu->addChild(mainClockScaleItem); + + OutputClockScalingItem* outputClockScaleItem = createMenuItem("Output clock", "▸"); + outputClockScaleItem->module = module; + menu->addChild(outputClockScaleItem); + + QuadraticGatesMenuItem* quadraticGatesItem = createMenuItem("Quadratic only mode", CHECKMARK(module->quadraticGatesOnly)); + quadraticGatesItem->module = module; + menu->addChild(quadraticGatesItem); + + menu->addChild(new MenuSeparator()); + + if (module->modeCOMIO == Muxlicer::COM_8_IN_1_OUT) { + OutputRangeItem* outputRangeItem = createMenuItem("All In Normalled Value", "▸"); + outputRangeItem->module = module; + menu->addChild(outputRangeItem); + } + else { + menu->addChild(createMenuLabel("All In Normalled Value (disabled)")); + } + + OutputClockStopStartItem* outputClockStopStartItem = + createMenuItem("Output clock follows play/stop", CHECKMARK(module->quadraticGatesOnly)); + outputClockStopStartItem->module = module; + menu->addChild(outputClockStopStartItem); + + menu->addChild(new MenuSeparator()); + menu->addChild(createMenuLabel("Input/Output mode")); + + IOMenuItem* ioItem = createMenuItem("1 input ▸ 8 outputs", + CHECKMARK(module->modeCOMIO == Muxlicer::COM_1_IN_8_OUT)); + ioItem->module = module; + ioItem->widget = this; + menu->addChild(ioItem); + + OIMenuItem* oiItem = createMenuItem("8 inputs ▸ 1 output", + CHECKMARK(module->modeCOMIO == Muxlicer::COM_8_IN_1_OUT)); + oiItem->module = module; + oiItem->widget = this; + menu->addChild(oiItem); + } + + void clearCables() { + for (int i = Muxlicer::MUX_OUTPUTS; i <= Muxlicer::MUX_OUTPUTS_LAST; ++i) { + APP->scene->rack->clearCablesOnPort(outputs[i]); + } + APP->scene->rack->clearCablesOnPort(inputs[Muxlicer::COM_INPUT]); + APP->scene->rack->clearCablesOnPort(inputs[Muxlicer::ALL_INPUT]); + + for (int i = Muxlicer::MUX_INPUTS; i <= Muxlicer::MUX_INPUTS_LAST; ++i) { + APP->scene->rack->clearCablesOnPort(inputs[i]); + } + APP->scene->rack->clearCablesOnPort(outputs[Muxlicer::COM_OUTPUT]); + APP->scene->rack->clearCablesOnPort(outputs[Muxlicer::ALL_OUTPUT]); + } + + // set ports visibility, either for 1 input -> 8 outputs or 8 inputs -> 1 output + void updatePortVisibilityForIOMode(Muxlicer::ModeCOMIO mode) { + + bool visibleToggle = (mode == Muxlicer::COM_1_IN_8_OUT); + + for (int i = Muxlicer::MUX_OUTPUTS; i <= Muxlicer::MUX_OUTPUTS_LAST; ++i) { + outputs[i]->visible = visibleToggle; + } + inputs[Muxlicer::COM_INPUT]->visible = visibleToggle; + outputs[Muxlicer::ALL_OUTPUT]->visible = visibleToggle; + + for (int i = Muxlicer::MUX_INPUTS; i <= Muxlicer::MUX_INPUTS_LAST; ++i) { + inputs[i]->visible = !visibleToggle; + } + outputs[Muxlicer::COM_OUTPUT]->visible = !visibleToggle; + inputs[Muxlicer::ALL_INPUT]->visible = !visibleToggle; + } + + +}; + + +Model* modelMuxlicer = createModel("Muxlicer"); + diff --git a/src/STMix.cpp b/src/STMix.cpp new file mode 100644 index 0000000..077d9aa --- /dev/null +++ b/src/STMix.cpp @@ -0,0 +1,133 @@ +#include "plugin.hpp" + +using simd::float_4; + +struct STMix : Module { + + // NOTE: not including auxiliary channel + static const int numMixerChannels = 4; + + enum ParamIds { + ENUMS(GAIN_PARAM, numMixerChannels), + NUM_PARAMS + }; + enum InputIds { + // +1 for aux + ENUMS(LEFT_INPUT, numMixerChannels + 1), + ENUMS(RIGHT_INPUT, numMixerChannels + 1), + NUM_INPUTS + }; + enum OutputIds { + LEFT_OUTPUT, + RIGHT_OUTPUT, + NUM_OUTPUTS + }; + enum LightIds { + ENUMS(LEFT_LED, 3), + ENUMS(RIGHT_LED, 3), + NUM_LIGHTS + }; + + STMix() { + config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); + for (int i = 0; i < numMixerChannels; ++i) { + configParam(GAIN_PARAM + i, 0.f, 1.f, 0.f, string::f("Gain %d", i + 1)); + } + } + + void process(const ProcessArgs& args) override { + float_4 out_left[4] = {}; + float_4 out_right[4] = {}; + + int numActivePolyphonyEngines = 1; + for (int i = 0; i < numMixerChannels + 1; ++i) { + const int stereoPolyChannels = std::max(inputs[LEFT_INPUT + i].getChannels(), + inputs[RIGHT_INPUT + i].getChannels()); + numActivePolyphonyEngines = std::max(numActivePolyphonyEngines, stereoPolyChannels); + } + + for (int i = 0; i < numMixerChannels + 1; ++i) { + + const float gain = (i < numMixerChannels) ? exponentialBipolar80Pade_5_4(params[GAIN_PARAM + i].getValue()) : 1.f; + + for (int c = 0; c < numActivePolyphonyEngines; c += 4) { + const float_4 in_left = inputs[LEFT_INPUT + i].getNormalVoltageSimd(0.f, c); + const float_4 in_right = inputs[RIGHT_INPUT + i].getNormalVoltageSimd(in_left, c); + + out_left[c / 4] += in_left * gain; + out_right[c / 4] += in_right * gain; + } + } + + outputs[LEFT_OUTPUT].setChannels(numActivePolyphonyEngines); + outputs[RIGHT_OUTPUT].setChannels(numActivePolyphonyEngines); + + for (int c = 0; c < numActivePolyphonyEngines; c += 4) { + outputs[LEFT_OUTPUT].setVoltageSimd(out_left[c / 4], c); + outputs[RIGHT_OUTPUT].setVoltageSimd(out_right[c / 4], c); + } + + if (numActivePolyphonyEngines == 1) { + lights[LEFT_LED + 0].setSmoothBrightness(outputs[LEFT_OUTPUT].getVoltage() / 5.f, args.sampleTime); + lights[RIGHT_LED + 0].setSmoothBrightness(outputs[RIGHT_OUTPUT].getVoltage() / 5.f, args.sampleTime); + lights[LEFT_LED + 1].setBrightness(0.f); + lights[RIGHT_LED + 1].setBrightness(0.f); + lights[LEFT_LED + 2].setBrightness(0.f); + lights[RIGHT_LED + 2].setBrightness(0.f); + } + else { + lights[LEFT_LED + 0].setBrightness(0.f); + lights[RIGHT_LED + 0].setBrightness(0.f); + lights[LEFT_LED + 1].setBrightness(0.f); + lights[RIGHT_LED + 1].setBrightness(0.f); + + float b_left = 0.f; + float b_right = 0.f; + for (int c = 0; c < numActivePolyphonyEngines; c++) { + b_left += std::pow(out_left[c / 4][c % 4], 2); + b_right += std::pow(out_right[c / 4][c % 4], 2); + } + b_left = std::sqrt(b_left) / 5.f; + b_right = std::sqrt(b_right) / 5.f; + lights[LEFT_LED + 2].setSmoothBrightness(b_left, args.sampleTime); + lights[RIGHT_LED + 2].setSmoothBrightness(b_right, args.sampleTime); + } + } +}; + + +struct STMixWidget : ModuleWidget { + STMixWidget(STMix* module) { + setModule(module); + setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/STMix.svg"))); + + addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); + addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + + addParam(createParamCentered(mm2px(Vec(21.0, 18.141)), module, STMix::GAIN_PARAM + 0)); + addParam(createParamCentered(mm2px(Vec(21.0, 41.451)), module, STMix::GAIN_PARAM + 1)); + addParam(createParamCentered(mm2px(Vec(21.0, 64.318)), module, STMix::GAIN_PARAM + 2)); + addParam(createParamCentered(mm2px(Vec(21.0, 87.124)), module, STMix::GAIN_PARAM + 3)); + + addInput(createInputCentered(mm2px(Vec(6.3, 13.108)), module, STMix::LEFT_INPUT + 0)); + addInput(createInputCentered(mm2px(Vec(6.3, 36.175)), module, STMix::LEFT_INPUT + 1)); + addInput(createInputCentered(mm2px(Vec(6.3, 59.243)), module, STMix::LEFT_INPUT + 2)); + addInput(createInputCentered(mm2px(Vec(6.3, 82.311)), module, STMix::LEFT_INPUT + 3)); + addInput(createInputCentered(mm2px(Vec(6.3, 105.378)), module, STMix::LEFT_INPUT + 4)); + + addInput(createInputCentered(mm2px(Vec(6.3, 23.108)), module, STMix::RIGHT_INPUT + 0)); + addInput(createInputCentered(mm2px(Vec(6.3, 46.354)), module, STMix::RIGHT_INPUT + 1)); + addInput(createInputCentered(mm2px(Vec(6.3, 69.237)), module, STMix::RIGHT_INPUT + 2)); + addInput(createInputCentered(mm2px(Vec(6.3, 92.132)), module, STMix::RIGHT_INPUT + 3)); + addInput(createInputCentered(mm2px(Vec(6.3, 115.379)), module, STMix::RIGHT_INPUT + 4)); + + addOutput(createOutputCentered(mm2px(Vec(23.8, 105.422)), module, STMix::LEFT_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(23.8, 115.392)), module, STMix::RIGHT_OUTPUT)); + + addChild(createLightCentered>(mm2px(Vec(16.8, 103.0)), module, STMix::LEFT_LED)); + addChild(createLightCentered>(mm2px(Vec(16.8, 113.0)), module, STMix::RIGHT_LED)); + } +}; + + +Model* modelSTMix = createModel("STMix"); \ No newline at end of file diff --git a/src/SamplingModulator.cpp b/src/SamplingModulator.cpp new file mode 100644 index 0000000..7837bfd --- /dev/null +++ b/src/SamplingModulator.cpp @@ -0,0 +1,310 @@ +#include "plugin.hpp" + + +struct SamplingModulator : Module { + + const static int numSteps = 8; + + enum ParamIds { + RATE_PARAM, + FINE_PARAM, + INT_EXT_PARAM, + ENUMS(STEP_PARAM, numSteps), + NUM_PARAMS + }; + enum InputIds { + SYNC_INPUT, + VOCT_INPUT, + HOLD_INPUT, + IN_INPUT, + NUM_INPUTS + }; + enum OutputIds { + CLOCK_OUTPUT, + TRIGG_OUTPUT, + OUT_OUTPUT, + NUM_OUTPUTS + }; + enum LightIds { + ENUMS(STEP_LIGHT, numSteps), + NUM_LIGHTS + }; + + enum StepState { + STATE_RESET, + STATE_OFF, + STATE_ON + }; + + enum ClockMode { + CLOCK_EXTERNAL, + CLOCK_INTERNAL + }; + + struct ClockTypeParam : ParamQuantity { + std::string getDisplayValueString() override { + if (module != nullptr && paramId == INT_EXT_PARAM) { + return (module->params[INT_EXT_PARAM].getValue() == CLOCK_EXTERNAL) ? "External" : "Internal"; + } + else { + return ""; + } + } + }; + + struct StepTypeParam : ParamQuantity { + std::string getDisplayValueString() override { + if (module != nullptr && STEP_PARAM <= paramId && STEP_PARAM < STEP_PARAM_LAST) { + StepState stepState = (StepState) module->params[paramId].getValue(); + + if (stepState == STATE_RESET) { + return "Reset"; + } + else if (stepState == STATE_OFF) { + return "Off"; + } + else { + return "On"; + } + } + else { + return ""; + } + } + }; + + int numEffectiveSteps = numSteps; + int currentStep = 0; + StepState stepStates[numSteps]; + + dsp::PulseGenerator triggerGenerator; + dsp::SchmittTrigger holdDetector; + dsp::SchmittTrigger clock; + dsp::MinBlepGenerator<16, 32> squareMinBlep; + dsp::MinBlepGenerator<16, 32> triggMinBlep; + dsp::MinBlepGenerator<16, 32> holdMinBlep; + bool removeDC = true; + + float stepPhase = 0.f; + float heldValue = 0.f; + /** Whether we are past the pulse width already */ + bool halfPhase = false; + + SamplingModulator() { + config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); + configParam(RATE_PARAM, 0.0f, 1.f, 0.f, "Rate"); + configParam(FINE_PARAM, 0.f, 1.f, 0.f, "Fine tune"); + configParam(INT_EXT_PARAM, 0.f, 1.f, CLOCK_INTERNAL, "Clock"); + + for (int i = 0; i < numSteps; i++) { + configParam(STEP_PARAM + i, 0.f, 2.f, STATE_ON, "Step " + std::to_string(i + 1)); + } + } + + void process(const ProcessArgs& args) override { + bool advanceStep = false; + + const bool isHoldOutRequired = outputs[OUT_OUTPUT].isConnected() && inputs[IN_INPUT].isConnected(); + const bool isClockOutRequired = outputs[CLOCK_OUTPUT].isConnected(); + const bool isTriggOutRequired = outputs[TRIGG_OUTPUT].isConnected(); + + if (params[INT_EXT_PARAM].getValue() == CLOCK_EXTERNAL) { + // if external mode, the SYNC/EXT. CLOCK input acts as a clock + advanceStep = clock.process(rescale(inputs[SYNC_INPUT].getVoltage(), 0.1f, 2.f, 0.f, 1.f)); + } + else { + // if internal mode, the SYNC/EXT. CLOCK input acts as oscillator sync, resetting the phase + if (clock.process(rescale(inputs[SYNC_INPUT].getVoltage(), 0.1f, 2.f, 0.f, 1.f))) { + advanceStep = true; + stepPhase = 0.f; + halfPhase = false; + } + } + + if (holdDetector.process(rescale(inputs[HOLD_INPUT].getVoltage(), 0.1f, 2.f, 0.f, 1.f))) { + float oldHeldValue = heldValue; + heldValue = inputs[IN_INPUT].getVoltage(); + holdMinBlep.insertDiscontinuity(0, heldValue - oldHeldValue); + } + + for (int i = 0; i < numSteps; i++) { + stepStates[i] = (StepState) params[STEP_PARAM + i].getValue(); + } + int numActiveSteps = 0; + numEffectiveSteps = 8; + for (int i = 0; i < numSteps; i++) { + numActiveSteps += (stepStates[i] == STATE_ON); + if (stepStates[i] == STATE_RESET) { + numEffectiveSteps = i; + break; + } + } + + const float pitch = 16.f * params[RATE_PARAM].getValue() + params[FINE_PARAM].getValue() + inputs[VOCT_INPUT].getVoltage(); + const float minDialFrequency = 1.0f; + const float frequency = minDialFrequency * simd::pow(2.f, pitch); + + const float oldPhase = stepPhase; + float deltaPhase = clamp(args.sampleTime * frequency, 1e-6f, 0.5f); + stepPhase += deltaPhase; + + if (!halfPhase && stepPhase >= 0.5) { + + float crossing = -(stepPhase - 0.5) / deltaPhase; + if (isClockOutRequired) { + squareMinBlep.insertDiscontinuity(crossing, -2.f); + } + if (isTriggOutRequired && stepStates[currentStep] == STATE_ON) { + triggMinBlep.insertDiscontinuity(crossing, -2.f); + } + + halfPhase = true; + } + + if (stepPhase >= 1.0f) { + stepPhase -= 1.0f; + + if (isClockOutRequired) { + float crossing = -stepPhase / deltaPhase; + squareMinBlep.insertDiscontinuity(crossing, +2.f); + } + + halfPhase = false; + + if (params[INT_EXT_PARAM].getValue() == CLOCK_INTERNAL) { + advanceStep = true; + } + } + + if (advanceStep) { + currentStep = (currentStep + 1) % std::max(1, numEffectiveSteps); + + if (stepStates[currentStep] == STATE_ON) { + const float crossing = -(oldPhase + deltaPhase - 1.0) / deltaPhase; + triggMinBlep.insertDiscontinuity(crossing, +2.f); + triggerGenerator.trigger(); + + if (!holdDetector.isHigh() && isHoldOutRequired) { + float oldHeldValue = heldValue; + heldValue = inputs[IN_INPUT].getVoltage(); + holdMinBlep.insertDiscontinuity(crossing, heldValue - oldHeldValue); + } + } + } + + const float holdOutput = isHoldOutRequired ? (heldValue + holdMinBlep.process()) : 0.f; + outputs[OUT_OUTPUT].setVoltage(holdOutput); + + if (isClockOutRequired) { + float square = (stepPhase < 0.5) ? 2.f : 0.f; + square += squareMinBlep.process(); + square -= 1.0f * removeDC; + outputs[CLOCK_OUTPUT].setVoltage(5.f * square); + } + else { + outputs[CLOCK_OUTPUT].setVoltage(0.f); + } + + if (params[INT_EXT_PARAM].getValue() == CLOCK_INTERNAL) { + if (isTriggOutRequired) { + float trigger = (stepPhase < 0.5 && stepStates[currentStep] == STATE_ON) ? 2.f : 0.f; + trigger += triggMinBlep.process(); + + if (removeDC) { + trigger -= 1.0f; + if (numEffectiveSteps > 0) { + trigger += (float)(numEffectiveSteps - numActiveSteps) / (numEffectiveSteps); + } + } + + outputs[TRIGG_OUTPUT].setVoltage(5.f * trigger); + } + else { + outputs[TRIGG_OUTPUT].setVoltage(0.f); + } + } + else { + // if externally clocked, just give standard triggers + outputs[TRIGG_OUTPUT].setVoltage(10.f * triggerGenerator.process(args.sampleTime)); + } + + for (int i = 0; i < numSteps; i++) { + lights[STEP_LIGHT + i].setBrightness(currentStep == i); + } + } + + void dataFromJson(json_t* rootJ) override { + json_t* modeJ = json_object_get(rootJ, "removeDC"); + removeDC = json_boolean_value(modeJ); + } + + json_t* dataToJson() override { + json_t* rootJ = json_object(); + json_object_set_new(rootJ, "removeDC", json_boolean(removeDC)); + return rootJ; + } +}; + + +struct SamplingModulatorWidget : ModuleWidget { + SamplingModulatorWidget(SamplingModulator* module) { + setModule(module); + setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/SamplingModulator.svg"))); + + addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); + addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); + addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + + addParam(createParamCentered(mm2px(Vec(9.72, 38.019)), module, SamplingModulator::RATE_PARAM)); + addParam(createParamCentered(mm2px(Vec(30.921, 38.019)), module, SamplingModulator::FINE_PARAM)); + addParam(createParamCentered(mm2px(Vec(20.313, 52.642)), module, SamplingModulator::INT_EXT_PARAM)); + addParam(createParamCentered(mm2px(Vec(8.319, 57.761)), module, SamplingModulator::STEP_PARAM + 0)); + addParam(createParamCentered(mm2px(Vec(8.319, 71.758)), module, SamplingModulator::STEP_PARAM + 1)); + addParam(createParamCentered(mm2px(Vec(8.319, 85.769)), module, SamplingModulator::STEP_PARAM + 2)); + addParam(createParamCentered(mm2px(Vec(8.319, 99.804)), module, SamplingModulator::STEP_PARAM + 3)); + addParam(createParamCentered(mm2px(Vec(32.326, 57.761)), module, SamplingModulator::STEP_PARAM + 4)); + addParam(createParamCentered(mm2px(Vec(32.326, 71.758)), module, SamplingModulator::STEP_PARAM + 5)); + addParam(createParamCentered(mm2px(Vec(32.326, 85.769)), module, SamplingModulator::STEP_PARAM + 6)); + addParam(createParamCentered(mm2px(Vec(32.326, 99.804)), module, SamplingModulator::STEP_PARAM + 7)); + + addInput(createInputCentered(mm2px(Vec(7.426, 16.737)), module, SamplingModulator::SYNC_INPUT)); + addInput(createInputCentered(mm2px(Vec(20.313, 28.175)), module, SamplingModulator::VOCT_INPUT)); + addInput(createInputCentered(mm2px(Vec(20.342, 111.762)), module, SamplingModulator::HOLD_INPUT)); + addInput(createInputCentered(mm2px(Vec(7.426, 114.484)), module, SamplingModulator::IN_INPUT)); + + addOutput(createOutputCentered(mm2px(Vec(20.313, 14.417)), module, SamplingModulator::CLOCK_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(33.224, 16.737)), module, SamplingModulator::TRIGG_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(33.224, 114.484)), module, SamplingModulator::OUT_OUTPUT)); + + addChild(createLightCentered>(mm2px(Vec(16.921, 62.208)), module, SamplingModulator::STEP_LIGHT + 0)); + addChild(createLightCentered>(mm2px(Vec(16.921, 73.011)), module, SamplingModulator::STEP_LIGHT + 1)); + addChild(createLightCentered>(mm2px(Vec(16.921, 83.814)), module, SamplingModulator::STEP_LIGHT + 2)); + addChild(createLightCentered>(mm2px(Vec(16.921, 94.617)), module, SamplingModulator::STEP_LIGHT + 3)); + addChild(createLightCentered>(mm2px(Vec(23.722, 62.208)), module, SamplingModulator::STEP_LIGHT + 4)); + addChild(createLightCentered>(mm2px(Vec(23.722, 73.011)), module, SamplingModulator::STEP_LIGHT + 5)); + addChild(createLightCentered>(mm2px(Vec(23.722, 83.814)), module, SamplingModulator::STEP_LIGHT + 6)); + addChild(createLightCentered>(mm2px(Vec(23.722, 94.617)), module, SamplingModulator::STEP_LIGHT + 7)); + } + + struct DCMenuItem : MenuItem { + SamplingModulator* module; + void onAction(const event::Action& e) override { + module->removeDC ^= true; + } + }; + + void appendContextMenu(Menu* menu) override { + SamplingModulator* module = dynamic_cast(this->module); + assert(module); + + menu->addChild(new MenuSeparator()); + + DCMenuItem* dcItem = createMenuItem("Remove DC Offset", CHECKMARK(module->removeDC)); + dcItem->module = module; + menu->addChild(dcItem); + } +}; + + +Model* modelSamplingModulator = createModel("SamplingModulator"); \ No newline at end of file diff --git a/src/plugin.cpp b/src/plugin.cpp index ec6cdcf..1f9f5e8 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -17,4 +17,10 @@ void init(rack::Plugin *p) { p->addModel(modelHexmixVCA); p->addModel(modelChoppingKinky); p->addModel(modelKickall); + p->addModel(modelSamplingModulator); + p->addModel(modelMorphader); + p->addModel(modelADSR); + p->addModel(modelSTMix); + p->addModel(modelMuxlicer); + p->addModel(modelMex); } diff --git a/src/plugin.hpp b/src/plugin.hpp index a6b8d71..2ca52ec 100644 --- a/src/plugin.hpp +++ b/src/plugin.hpp @@ -1,23 +1,29 @@ +#pragma once #include using namespace rack; -extern Plugin *pluginInstance; - -extern Model *modelEvenVCO; -extern Model *modelRampage; -extern Model *modelABC; -extern Model *modelSpringReverb; -extern Model *modelMixer; -extern Model *modelSlewLimiter; -extern Model *modelDualAtenuverter; -extern Model *modelPercall; -extern Model *modelHexmixVCA; -extern Model *modelChoppingKinky; -extern Model *modelKickall; - +extern Plugin* pluginInstance; + +extern Model* modelEvenVCO; +extern Model* modelRampage; +extern Model* modelABC; +extern Model* modelSpringReverb; +extern Model* modelMixer; +extern Model* modelSlewLimiter; +extern Model* modelDualAtenuverter; +extern Model* modelPercall; +extern Model* modelHexmixVCA; +extern Model* modelChoppingKinky; +extern Model* modelKickall; +extern Model* modelSamplingModulator; +extern Model* modelMorphader; +extern Model* modelADSR; +extern Model* modelSTMix; +extern Model* modelMuxlicer; +extern Model* modelMex; struct Knurlie : SvgScrew { Knurlie() { @@ -43,11 +49,27 @@ struct BefacoTinyKnobWhite : app::SvgKnob { } }; -struct BefacoTinyKnobGrey : app::SvgKnob { - BefacoTinyKnobGrey() { +struct BefacoTinyKnobDarkGrey : app::SvgKnob { + BefacoTinyKnobDarkGrey() { minAngle = -0.8 * M_PI; maxAngle = 0.8 * M_PI; - setSvg(APP->window->loadSvg(asset::plugin(pluginInstance, "res/BefacoTinyKnobGrey.svg"))); + setSvg(APP->window->loadSvg(asset::plugin(pluginInstance, "res/BefacoTinyKnobDarkGrey.svg"))); + } +}; + +struct BefacoTinyKnobLightGrey : app::SvgKnob { + BefacoTinyKnobLightGrey() { + minAngle = -0.8 * M_PI; + maxAngle = 0.8 * M_PI; + setSvg(APP->window->loadSvg(asset::plugin(pluginInstance, "res/BefacoTinyKnobLightGrey.svg"))); + } +}; + +struct BefacoTinyKnobBlack : app::SvgKnob { + BefacoTinyKnobBlack() { + minAngle = -0.8 * M_PI; + maxAngle = 0.8 * M_PI; + setSvg(APP->window->loadSvg(asset::plugin(pluginInstance, "res/BefacoTinyKnobBlack.svg"))); } }; @@ -57,6 +79,25 @@ struct Davies1900hLargeGreyKnob : Davies1900hKnob { } }; +struct Davies1900hLightGreyKnob : Davies1900hWhiteKnob { + Davies1900hLightGreyKnob() { + setSvg(APP->window->loadSvg(asset::plugin(pluginInstance, "res/Davies1900hLightGrey.svg"))); + } +}; + +struct Davies1900hDarkGreyKnob : Davies1900hWhiteKnob { + Davies1900hDarkGreyKnob() { + setSvg(APP->window->loadSvg(asset::plugin(pluginInstance, "res/Davies1900hDarkGrey.svg"))); + } +}; + +// library black Davies1900h doesn't work well on black backgrounds +struct Davies1900hDarkBlackAlt : Davies1900hWhiteKnob { + Davies1900hDarkBlackAlt() { + setSvg(APP->window->loadSvg(asset::plugin(pluginInstance, "res/Davies1900hBlack.svg"))); + } +}; + struct BefacoOutputPort : app::SvgPort { BefacoOutputPort() { setSvg(APP->window->loadSvg(asset::plugin(pluginInstance, "res/BefacoOutputPort.svg"))); @@ -69,6 +110,35 @@ struct BefacoInputPort : app::SvgPort { } }; +struct CKSSNarrow : app::SvgSwitch { + CKSSNarrow() { + addFrame(APP->window->loadSvg(asset::plugin(pluginInstance, "res/SwitchNarrow_0.svg"))); + addFrame(APP->window->loadSvg(asset::plugin(pluginInstance, "res/SwitchNarrow_1.svg"))); + } +}; + +struct Crossfader : app::SvgSlider { + Crossfader() { + setBackgroundSvg(APP->window->loadSvg(asset::plugin(pluginInstance, "res/CrossfaderBackground.svg"))); + setHandleSvg(APP->window->loadSvg(asset::plugin(pluginInstance, "res/CrossfaderHandle.svg"))); + minHandlePos = mm2px(Vec(4.5f, -0.8f)); + maxHandlePos = mm2px(Vec(34.5, -0.8f)); + horizontal = true; + math::Vec margin = math::Vec(15, 5); + background->box.pos = margin; + box.size = background->box.size.plus(margin.mult(2)); + } +}; + +struct BefacoSwitchHorizontal : app::SvgSwitch { + BefacoSwitchHorizontal() { + addFrame(APP->window->loadSvg(asset::plugin(pluginInstance, "res/BefacoSwitchHoriz_0.svg"))); + addFrame(APP->window->loadSvg(asset::plugin(pluginInstance, "res/BefacoSwitchHoriz_1.svg"))); + addFrame(APP->window->loadSvg(asset::plugin(pluginInstance, "res/BefacoSwitchHoriz_2.svg"))); + } +}; + + template T sin2pi_pade_05_5_4(T x) { x -= 0.5f; @@ -83,6 +153,11 @@ T tanh_pade(T x) { return 12.f * x * q / (36.f * x2 + q * q); } +template +T exponentialBipolar80Pade_5_4(T x) { + return (T(0.109568) * x + T(0.281588) * simd::pow(x, 3) + T(0.133841) * simd::pow(x, 5)) + / (T(1.) - T(0.630374) * simd::pow(x, 2) + T(0.166271) * simd::pow(x, 4)); +} struct ADEnvelope { enum Stage { @@ -131,4 +206,4 @@ struct ADEnvelope { private: float envLinear = 0.f; -}; +}; \ No newline at end of file