diff --git a/src/Muxlicer.cpp b/src/Muxlicer.cpp index 43d76cc..8a57d84 100644 --- a/src/Muxlicer.cpp +++ b/src/Muxlicer.cpp @@ -113,8 +113,8 @@ struct MultDivClock { float dividedProgressSeconds = 0.f; - // returns the gated clock signal - float process(float deltaTime, bool clockPulseReceived) { + // 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 @@ -124,7 +124,7 @@ struct MultDivClock { secondsSinceLastClock = 0.0f; } - float out = 0.f; + bool out = false; if (secondsSinceLastClock >= 0.0f) { secondsSinceLastClock += deltaTime; @@ -159,7 +159,7 @@ struct MultDivClock { float multipliedProgressSeconds = dividedProgressSeconds / multipliedSeconds; multipliedProgressSeconds -= (float)(int)multipliedProgressSeconds; multipliedProgressSeconds *= multipliedSeconds; - out += (float)(multipliedProgressSeconds <= gateSeconds); + out = (multipliedProgressSeconds <= gateSeconds); } } return out; @@ -178,7 +178,10 @@ struct MultDivClock { } }; -static const std::vector clockOptions = {-16, -8, -4, -3, -2, 1, 2, 3, 4, 8, 16}; +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)); @@ -189,7 +192,7 @@ struct Muxlicer : Module { PLAY_PARAM, ADDRESS_PARAM, GATE_MODE_PARAM, - TAP_TEMPO_PARAM, + DIV_MULT_PARAM, ENUMS(LEVEL_PARAMS, 8), NUM_PARAMS }; @@ -244,8 +247,9 @@ struct Muxlicer : Module { 7 seven gates | x 8 eight gates | ✔ */ - int possibleQuadraticGates[5] = {-1, 1, 2, 4, 8}; + const int possibleQuadraticGates[5] = {-1, 1, 2, 4, 8}; bool quadraticGatesOnly = false; + bool outputClockFollowsPlayMode = false; PlayState playState = STATE_STOPPED; dsp::BooleanTrigger playStateTrigger; @@ -262,7 +266,7 @@ struct Muxlicer : Module { float tapTime = 99999; // used to track the time between clock pulses (or taps?) dsp::SchmittTrigger inputClockTrigger; // to detect incoming clock pulses - dsp::SchmittTrigger mainClockTrigger; // to detect rising edges from the divided/multiplied version of the clock signal + 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 @@ -277,12 +281,11 @@ struct Muxlicer : Module { int allInNormalVoltage = 10; // what voltage is normalled into the "All In" input, selectable via context menu Module* rightModule; // for the expander - struct TapTempoKnobParamQuantity : ParamQuantity { + struct DivMultKnobParamQuantity : ParamQuantity { std::string getDisplayValueString() override { - if (module != nullptr) { - - const int clockOptionIndex = clamp(int(ParamQuantity::getValue()), 0, clockOptions.size()); - return getClockOptionString(clockOptions[clockOptionIndex]); + Muxlicer* moduleMuxlicer = reinterpret_cast(module); + if (moduleMuxlicer != nullptr) { + return getClockOptionString(moduleMuxlicer->getClockOptionFromParam()); } else { return ""; @@ -290,13 +293,71 @@ struct Muxlicer : Module { } }; + 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, 0.f, "Gate mode"); - const int numClockOptions = clockOptions.size(); - configParam(Muxlicer::TAP_TEMPO_PARAM, 0, numClockOptions - 1, numClockOptions / 2, "Main clock mult/div"); + 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("Slider %d", i)); @@ -317,7 +378,7 @@ struct Muxlicer : Module { 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/drive the clock (if clock input present) + // 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; } @@ -326,9 +387,8 @@ struct Muxlicer : Module { externalClockPulseReceived = true; tapped = false; } - - const int clockOptionFromDial = clockOptions[int(params[TAP_TEMPO_PARAM].getValue())]; - mainClockMultDiv.multDiv = clockOptionFromDial; + + mainClockMultDiv.multDiv = getClockOptionFromParam(); if (resetTrigger.process(rescale(inputs[RESET_INPUT].getVoltage(), 0.1f, 2.f, 0.f, 1.f))) { reset = true; @@ -343,10 +403,10 @@ struct Muxlicer : Module { const bool isSequenceAdvancing = 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 after CLOCK_INPUT is disconnected + // 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 button (if sufficiently short) + // of the tap-tempo menu item (if sufficiently short) if (usingExternalClock || tapTime < 2.f) { internalClockLength = tapTime; } @@ -368,8 +428,8 @@ struct Muxlicer : Module { // // 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 - this outputs a gate sequence - // so we must use a Schmitt Trigger on the divided/mult'd signal in order to detect when to advance the sequence + // 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)); // reset _doesn't_ reset/sync the clock, it just moves the sequence index marker back to the start @@ -384,7 +444,7 @@ struct Muxlicer : Module { if (dividedMultipliedClockPulseReceived) { if (isSequenceAdvancing && !resetGracePeriodActive) { runIndex++; - if (runIndex >= 8) { + if (runIndex >= SEQUENCE_LENGTH) { // both play modes will reset to step 0 and fire an EOC trigger runIndex = 0; endOfCyclePulse.trigger(1e-3); @@ -395,7 +455,6 @@ struct Muxlicer : Module { } } } - multiClock.reset(mainClockMultDiv.getEffectiveClockLength()); for (int i = 0; i < 8; i++) { @@ -407,7 +466,7 @@ struct Muxlicer : Module { addressIndex = runIndex; } else { - addressIndex = clamp((int) roundf(address), 0, 8 - 1); + addressIndex = clamp((int) roundf(address), 0, SEQUENCE_LENGTH - 1); } // Gates @@ -428,7 +487,6 @@ struct Muxlicer : Module { outputs[ALL_GATES_OUTPUT].setVoltage(gateValue); } - 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(); @@ -466,7 +524,9 @@ struct Muxlicer : Module { outputs[COM_OUTPUT].setChannels(numActiveEngines); } - const bool isOutputClockHigh = outputClockMultDiv.process(args.sampleTime, clockPulseReceived); + // there is an option to stop output clock when play stops + const bool playStateMask = !outputClockFollowsPlayMode || (playState != STATE_STOPPED); + const bool isOutputClockHigh = outputClockMultDiv.process(args.sampleTime, clockPulseReceived) && playStateMask; outputs[CLOCK_OUTPUT].setVoltage(isOutputClockHigh ? 10.f : 0.f); lights[CLOCK_LIGHT].setBrightness(isOutputClockHigh ? 1.f : 0.f); @@ -561,6 +621,7 @@ struct Muxlicer : Module { 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; } @@ -583,6 +644,11 @@ struct Muxlicer : Module { json_t* playStateJ = json_object_get(rootJ, "playState"); playState = (PlayState) json_integer_value(playStateJ); + + json_t* outputClockFollowsPlayModeJ = json_object_get(rootJ, "outputClockFollowsPlayMode"); + outputClockFollowsPlayMode = json_boolean_value(outputClockFollowsPlayModeJ); + + updateParamFromMainClockMultDiv(); } }; @@ -601,9 +667,9 @@ struct MuxlicerWidget : ModuleWidget { 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::TAP_TEMPO_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)); @@ -726,8 +792,6 @@ struct MuxlicerWidget : ModuleWidget { } }; - - struct OutputClockScalingItem : MenuItem { Muxlicer* module; @@ -742,7 +806,7 @@ struct MuxlicerWidget : ModuleWidget { Menu* createChildMenu() override { Menu* menu = new Menu; - for (int clockOption : clockOptions) { + for (int clockOption : module->quadraticGatesOnly ? clockOptionsQuadratic : clockOptionsAll) { std::string optionString = getClockOptionString(clockOption); OutputClockScalingChildItem* clockItem = createMenuItem(optionString, CHECKMARK(module->outputClockMultDiv.multDiv == clockOption)); @@ -764,7 +828,7 @@ struct MuxlicerWidget : ModuleWidget { void onAction(const event::Action& e) override { module->mainClockMultDiv.multDiv = mainClockMulDiv; - module->params[Muxlicer::TAP_TEMPO_PARAM].setValue(mainClockMulDivIndex); + module->updateParamFromMainClockMultDiv(); } }; @@ -772,7 +836,8 @@ struct MuxlicerWidget : ModuleWidget { Menu* menu = new Menu; int i = 0; - for (int clockOption : clockOptions) { + + for (int clockOption : module->quadraticGatesOnly ? clockOptionsQuadratic : clockOptionsAll) { std::string optionString = getClockOptionString(clockOption); MainClockScalingChildItem* clockItem = createMenuItem(optionString, CHECKMARK(module->mainClockMultDiv.multDiv == clockOption)); @@ -792,6 +857,15 @@ struct MuxlicerWidget : ModuleWidget { 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(); } }; @@ -829,15 +903,20 @@ struct MuxlicerWidget : ModuleWidget { 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()); OutputRangeItem* outputRangeItem = createMenuItem("All In Normalled Value", "▸"); outputRangeItem->module = module; menu->addChild(outputRangeItem); - QuadraticGatesMenuItem* quadraticGatesItem = createMenuItem("Gate Mode: quadratic only", CHECKMARK(module->quadraticGatesOnly)); - quadraticGatesItem->module = module; - menu->addChild(quadraticGatesItem); + 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"));