Browse Source

Muxlicer improvements

* improving input/output clock div/mult to respect quadratic mode
* fix knob colour
* better tooltips for gate mode
* output clock option to follow play/stop
tags/v2.1.0
hemmer 3 years ago
parent
commit
7c9fcae02b
1 changed files with 119 additions and 40 deletions
  1. +119
    -40
      src/Muxlicer.cpp

+ 119
- 40
src/Muxlicer.cpp View File

@@ -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<int> clockOptions = {-16, -8, -4, -3, -2, 1, 2, 3, 4, 8, 16};
static const std::vector<int> clockOptionsQuadratic = {-16, -8, -4, -2, 1, 2, 4, 8, 16};
static const std::vector<int> 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<Muxlicer*>(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<Muxlicer*>(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<TapTempoKnobParamQuantity>(Muxlicer::TAP_TEMPO_PARAM, 0, numClockOptions - 1, numClockOptions / 2, "Main clock mult/div");
configParam<GateModeParamQuantity>(Muxlicer::GATE_MODE_PARAM, -1.f, 8.f, 1.f, "Gate mode");
configParam<DivMultKnobParamQuantity>(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<Knurlie>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));

addParam(createParam<BefacoSwitchMomentary>(mm2px(Vec(35.72963, 10.008)), module, Muxlicer::PLAY_PARAM));
addParam(createParam<BefacoTinyKnobSnap>(mm2px(Vec(3.84112, 10.90256)), module, Muxlicer::ADDRESS_PARAM));
addParam(createParam<BefacoTinyKnobWhite>(mm2px(Vec(67.83258, 10.86635)), module, Muxlicer::GATE_MODE_PARAM));
addParam(createParam<BefacoTinyKnobSnap>(mm2px(Vec(28.12238, 24.62151)), module, Muxlicer::TAP_TEMPO_PARAM));
addParam(createParam<BefacoTinyKnobDarkGrey>(mm2px(Vec(3.84112, 10.90256)), module, Muxlicer::ADDRESS_PARAM));
addParam(createParam<BefacoTinyKnobDarkGrey>(mm2px(Vec(67.83258, 10.86635)), module, Muxlicer::GATE_MODE_PARAM));
addParam(createParam<BefacoTinyKnob>(mm2px(Vec(28.12238, 24.62151)), module, Muxlicer::DIV_MULT_PARAM));

addParam(createParam<BefacoSlidePot>(mm2px(Vec(2.32728, 40.67102)), module, Muxlicer::LEVEL_PARAMS + 0));
addParam(createParam<BefacoSlidePot>(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<OutputClockScalingChildItem>(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<MainClockScalingChildItem>(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<QuadraticGatesMenuItem>("Quadratic only mode", CHECKMARK(module->quadraticGatesOnly));
quadraticGatesItem->module = module;
menu->addChild(quadraticGatesItem);

menu->addChild(new MenuSeparator());

OutputRangeItem* outputRangeItem = createMenuItem<OutputRangeItem>("All In Normalled Value", "▸");
outputRangeItem->module = module;
menu->addChild(outputRangeItem);

QuadraticGatesMenuItem* quadraticGatesItem = createMenuItem<QuadraticGatesMenuItem>("Gate Mode: quadratic only", CHECKMARK(module->quadraticGatesOnly));
quadraticGatesItem->module = module;
menu->addChild(quadraticGatesItem);
OutputClockStopStartItem* outputClockStopStartItem =
createMenuItem<OutputClockStopStartItem>("Output clock follows play/stop", CHECKMARK(module->quadraticGatesOnly));
outputClockStopStartItem->module = module;
menu->addChild(outputClockStopStartItem);

menu->addChild(new MenuSeparator());
menu->addChild(createMenuLabel<MenuLabel>("Input/Output mode"));


Loading…
Cancel
Save