diff --git a/plugin.json b/plugin.json index 42c4844..9b6ec77 100644 --- a/plugin.json +++ b/plugin.json @@ -151,6 +151,18 @@ "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": [ + "Sample and hold", + "Oscillator", + "Clock generator" + ] } ] } 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/src/SamplingModulator.cpp b/src/SamplingModulator.cpp new file mode 100644 index 0000000..d805d50 --- /dev/null +++ b/src/SamplingModulator.cpp @@ -0,0 +1,265 @@ +#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]; + float triggerTime = 0; + bool triggerActive = false; + dsp::SchmittTrigger holdDetector; + dsp::SchmittTrigger clock; + dsp::MinBlepGenerator<16, 32> squareMinBlep; + dsp::MinBlepGenerator<16, 32> triggMinBlep; + dsp::MinBlepGenerator<16, 32> holdMinBlep; + bool applyMinBlep = 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; + holdDetector.process(rescale(inputs[HOLD_INPUT].getVoltage(), 0.1f, 2.f, 0.f, 1.f)); + + 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 a sync + if (clock.process(rescale(inputs[SYNC_INPUT].getVoltage(), 0.1f, 2.f, 0.f, 1.f))) { + currentStep = 0; + stepPhase = 0.f; + halfPhase = false; + } + } + + for (int i = 0; i < numSteps; i++) { + stepStates[i] = (StepState) params[STEP_PARAM + i].getValue(); + } + numEffectiveSteps = 8; + for (int i = 0; i < numSteps; i++) { + 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 frequency = 0.1 * simd::pow(2.f, pitch); + + 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; + squareMinBlep.insertDiscontinuity(crossing, -2.f); + halfPhase = true; + } + + if (stepPhase >= 1.0f) { + stepPhase -= 1.0f; + float crossing = -stepPhase / deltaPhase; + squareMinBlep.insertDiscontinuity(crossing, +2.f); + + halfPhase = false; + + if (params[INT_EXT_PARAM].getValue() == CLOCK_INTERNAL) { + advanceStep = true; + } + } + + if (triggerActive) { + triggerTime -= args.sampleTime; + if (triggerTime < 0) { + triggMinBlep.insertDiscontinuity(triggerTime, -2.f); + triggerTime = 0.; + triggerActive = false; + } + } + + if (advanceStep) { + // TODO: what does reset on first step do? + currentStep = (currentStep + 1) % std::max(1, numEffectiveSteps); + + if (stepStates[currentStep] == STATE_ON) { + + float oldHeldValue = heldValue; + heldValue = inputs[IN_INPUT].getVoltage();; + triggerTime = 1e-3; + triggerActive = true; + + float crossing = -(oldPhase + deltaPhase - 1.0) / deltaPhase; + // TODO: i guess should only be on if clock is internal? + triggMinBlep.insertDiscontinuity(crossing, +2.f); + holdMinBlep.insertDiscontinuity(crossing, heldValue - oldHeldValue); + } + } + + float output = heldValue + holdMinBlep.process() * applyMinBlep; + outputs[OUT_OUTPUT].setVoltage(output); + + // TODO: could calculate DC offset correction based on number of active bits + float triggerOut = triggerActive ? +1.f : -1.f; + triggerOut += triggMinBlep.process() * applyMinBlep; + outputs[TRIGG_OUTPUT].setVoltage(5.f * triggerOut); + + float square = (stepPhase < 0.5) ? 1.f : -1.f; + square += squareMinBlep.process() * applyMinBlep; + outputs[CLOCK_OUTPUT].setVoltage(5.f * square); + + for (int i = 0; i < numSteps; i++) { + lights[STEP_LIGHT + i].setBrightness(currentStep == i); + } + } +}; + + +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 MinBLEPMenuItem : MenuItem { + SamplingModulator* module; + void onAction(const event::Action& e) override { + module->applyMinBlep ^= true; + } + }; + + void appendContextMenu(Menu* menu) override { + SamplingModulator* module = dynamic_cast(this->module); + assert(module); + + menu->addChild(new MenuSeparator()); + + MinBLEPMenuItem* minBlepItem = createMenuItem("Apply minBlep", CHECKMARK(module->applyMinBlep)); + minBlepItem->module = module; + menu->addChild(minBlepItem); + } + +}; + + +Model* modelSamplingModulator = createModel("SamplingModulator"); \ No newline at end of file diff --git a/src/plugin.cpp b/src/plugin.cpp index ec6cdcf..4e90298 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -17,4 +17,5 @@ void init(rack::Plugin *p) { p->addModel(modelHexmixVCA); p->addModel(modelChoppingKinky); p->addModel(modelKickall); + p->addModel(modelSamplingModulator); } diff --git a/src/plugin.hpp b/src/plugin.hpp index a6b8d71..c1590ae 100644 --- a/src/plugin.hpp +++ b/src/plugin.hpp @@ -17,6 +17,7 @@ extern Model *modelPercall; extern Model *modelHexmixVCA; extern Model *modelChoppingKinky; extern Model *modelKickall; +extern Model *modelSamplingModulator; struct Knurlie : SvgScrew {