#include "Noobhour.hpp" #include "dsp/digital.hpp" #include namespace rack_plugin_noobhour { // TODO LATER // scale file loader - Latif Fital // CustomScale Pro // - paging? different pages, back/forth, CV for index selection (interpolate between non-empty pages, lights to indicate?) // - quad // - button back for randomization // TODO // IN PROGRESS // bus // DONE // bsl1r // context menu switch between 0-10 and -5..5 - (was attenuator - Pyer Cllrd) // Normalizing the gate input to all following inputs (with nothing plugged in them) would be amazing (instead of 4 copies of the same cable) Patrick McIlveen // Normalized HIGH and LOW inputs as well // Normalizing all of the outputs to the last one (a la AS 4ch baby mixer and Audible) Patrick McIlveen // Performance imporvement CustomScale // random subset (randomize activity of individual tones) - Pyer Cllrd // ended up with lines on I, IV, V, was: change rings around lights for black/white distinction - steve baker struct GreenBlueYellowLight : GrayModuleLightWidget { GreenBlueYellowLight() { addBaseColor(COLOR_GREEN); addBaseColor(COLOR_BLUE); addBaseColor(COLOR_YELLOW); } }; template struct ToneLight : BASE { ToneLight() { this->box.size = mm2px(Vec(6.0f, 6.0f)); } }; /* struct LEDBezelGray : SVGSwitch, MomentarySwitch { LEDBezelGray() { addFrame(SVG::load(assetPlugin(plugin, "res/LEDBezelGray.svg"))); } }; struct LEDBezelDark : SVGSwitch, MomentarySwitch { LEDBezelDark() { addFrame(SVG::load(assetPlugin(plugin, "res/LEDBezelDark.svg"))); } }; struct LighterGrayModuleLightWidget : ModuleLightWidget { LighterGrayModuleLightWidget() { bgColor = nvgRGB(0x9a, 0x9a, 0x9a); borderColor = nvgRGBA(0, 0, 0, 0x60); } }; struct LighterGreenBlueYellowLight : LighterGrayModuleLightWidget { LighterGreenBlueYellowLight() { addBaseColor(COLOR_GREEN); addBaseColor(COLOR_BLUE); addBaseColor(COLOR_YELLOW); } }; */ struct Customscaler : Module { static const int NUM_OCTAVES = 5; static const int BASE_OCTAVE = 2; static const int NUM_TONES = NUM_OCTAVES * 12; enum InputIds { SIGNAL_INPUT, TONE_INPUT, TOGGLE_TRIGGER_INPUT, RESET_TRIGGER_INPUT, RANDOMIZE_TRIGGER_INPUT, P_INPUT, BASE_INPUT, NUM_INPUTS }; enum OutputIds { OUT_OUTPUT, CHANGEGATE_OUTPUT, NUM_OUTPUTS }; enum ParamIds { ENUMS(TONE1_PARAM, NUM_TONES), RANGE_PARAM, P_PARAM, // RANDOMIZE_BUTTON_PARAM, RESET_BUTTON_PARAM, BASE_PARAM, MODE_PARAM, NUM_PARAMS }; enum LightIds { ENUMS(TONE1_LIGHT, NUM_TONES * 3), NUM_LIGHTS }; SchmittTrigger gateTrigger; SchmittTrigger randomizeTrigger; SchmittTrigger resetTrigger; SchmittTrigger resetButtonTrigger; SchmittTrigger paramTrigger[NUM_TONES]; PulseGenerator changePulse; bool state[NUM_TONES]; bool candidate[NUM_TONES]; int lastFinalTone = NUM_TONES; int lastStartTone = NUM_TONES; int lastSelectedTone = NUM_TONES; std::vector activeTones; bool activeTonesDirty = true; bool bipolarInput = false; Customscaler() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) { activeTones.reserve(NUM_TONES); onReset(); } void step() override; float getVOct(int toneIndex) const { int octave = toneIndex / 12; int tone = toneIndex % 12; return tone/12.f + octave - BASE_OCTAVE; } int getTone(float vOct) const { return static_cast(vOct * 12.f) + 12 * BASE_OCTAVE; } void onReset() override { for (int i = 0; i < NUM_TONES; i++) { state[i] = false; candidate[i] = false; } activeTonesDirty = true; } float getP() { float p_input = 0; if (inputs[P_INPUT].active) p_input = clamp(inputs[P_INPUT].value / 10.f, -10.f, 10.f); return clamp(p_input + params[P_PARAM].value, 0.0f, 1.0f); } void onRandomize() override { randomizeTones(getP()); } void randomizeTones(float p) { for (int i = 0; i < NUM_TONES; i++) { state[i] = (randomUniform() < p); candidate[i] = false; } activeTonesDirty = true; } void randomSubset(float p) { int activeTones = 0; int candidates = 0; bool toggle = params[MODE_PARAM].value < 0.5f; for (int i = 0; i < NUM_TONES; i++) { if (state[i] || candidate[i]) { candidates++; if (toggle) { if (randomUniform() < p) { state[i] ^= true; } if (state[i]) { activeTones++; candidate[i] = false; } else { candidate[i] = true; } } else { if (randomUniform() < p) { activeTones++; state[i] = true; candidate[i] = false; } else { state[i] = false; candidate[i] = true; } } } } // if random subset is called without active or candidate tones, // let it behave like the normal randomisation: everything is a // candidate, retry if (candidates == 0) { for (int i = 0; i < NUM_TONES; i++) { candidate[i] = true; } randomSubset(p); return; } // make sure at least one tone is active so we don't return 0 = C4 // which may be not be a candidate if (activeTones == 0) { for (int i = 0; i < NUM_TONES; i++) { if (candidate[i]) { state[i] = true; candidate[i] = false; break; } } } activeTonesDirty = true; } json_t *toJson() override { json_t *rootJ = json_object(); json_t *statesJ = json_array(); for (int i = 0; i < NUM_TONES; i++) { json_t *stateJ = json_boolean(state[i]); json_array_append_new(statesJ, stateJ); } json_object_set_new(rootJ, "states", statesJ); json_t *candidatesJ = json_array(); for (int i = 0; i < NUM_TONES; i++) { json_t *candidateJ = json_boolean(candidate[i]); json_array_append_new(candidatesJ, candidateJ); } json_object_set_new(rootJ, "candidates", candidatesJ); json_t *bipolarInputJ = json_boolean(bipolarInput); json_object_set_new(rootJ, "bipolarInput", bipolarInputJ); return rootJ; } void fromJson(json_t *rootJ) override { json_t *statesJ = json_object_get(rootJ, "states"); if (statesJ) { for (int i = 0; i < NUM_TONES; i++) { json_t *stateJ = json_array_get(statesJ, i); if (stateJ) state[i] = json_boolean_value(stateJ); } } json_t *candidatesJ = json_object_get(rootJ, "candidates"); if (candidatesJ) { for (int i = 0; i < NUM_TONES; i++) { json_t *candidateJ = json_array_get(candidatesJ, i); if (candidateJ) candidate[i] = json_boolean_value(candidateJ); } } json_t *bipolarInputJ = json_object_get(rootJ, "bipolarInput"); bipolarInput = json_boolean_value(bipolarInputJ); activeTonesDirty = true; } }; void Customscaler::step() { // RESET if (inputs[RESET_TRIGGER_INPUT].active) { if (resetTrigger.process(rescale(inputs[RESET_TRIGGER_INPUT].value, 0.1f, 2.f, 0.f, 1.f))) { onReset(); } } if (resetButtonTrigger.process(params[RESET_BUTTON_PARAM].value)) { onReset(); } // RANDOMIZE if (inputs[RANDOMIZE_TRIGGER_INPUT].active) { if (randomizeTrigger.process(rescale(inputs[RANDOMIZE_TRIGGER_INPUT].value, 0.1f, 2.f, 0.f, 1.f))) { randomSubset(getP()); } } /* if (randomizeButtonTrigger.process(params[RANDOMIZE_BUTTON_PARAM].value)) { randomizeTones(getP()); } */ // TOGGLE if (inputs[TONE_INPUT].active) { float gate = 0.0; if (inputs[TOGGLE_TRIGGER_INPUT].active) gate = inputs[TOGGLE_TRIGGER_INPUT].value; if (gateTrigger.process(rescale(gate, 0.1f, 2.f, 0.f, 1.f))) { int toneIndex = getTone(inputs[TONE_INPUT].value); if (toneIndex >= 0 && toneIndex < NUM_TONES) { state[toneIndex] ^= true; candidate[toneIndex] = false; activeTonesDirty = true; } } } // OCTAVE RANGE int startTone = 0; int endTone = 0; if (params[RANGE_PARAM].value < 0.5f) { startTone = 0; endTone = NUM_TONES - 1; } else if (params[RANGE_PARAM].value < 1.5f) { startTone = 12; endTone = NUM_TONES - 13; } else { startTone = 24; endTone = NUM_TONES - 25; } if (startTone != lastStartTone) { activeTonesDirty = true; lastStartTone = startTone; } // CHECK TONE TOGGLES for (int i = 0; i < NUM_TONES; i++) { if (paramTrigger[i].process(params[i].value)) { state[i] ^= true; candidate[i] = false; activeTonesDirty = true; } } // GATHER CANDIDATES if (activeTonesDirty) { activeTones.clear(); for (int i = 0; i < NUM_TONES; i++) { if (state[i] && i >= startTone && i <= endTone) { activeTones.push_back(i); } } } // FETCH BASE TONE float baseTone = params[BASE_PARAM].value; if (inputs[BASE_INPUT].active) { baseTone += inputs[BASE_INPUT].value / 10.f * 11.f; } int baseToneDiscrete = static_cast(clamp(baseTone, 0.f, 11.f)); // SELECT TONE float output = 0; int selectedTone = NUM_TONES; int finalTone = NUM_TONES; if (inputs[SIGNAL_INPUT].active && activeTones.size() > 0) { float inp = inputs[SIGNAL_INPUT].value; if (bipolarInput) inp += 5.f; unsigned int selectedIndex = static_cast(activeTones.size() * (clamp(inp, 0.f, 10.f)) / 10.f); if (selectedIndex == activeTones.size()) selectedIndex--; selectedTone = activeTones[selectedIndex]; finalTone = selectedTone + baseToneDiscrete; output = getVOct(finalTone); } // DETECT TONE CHANGE if (finalTone != lastFinalTone) { changePulse.trigger(0.001f); lastFinalTone = finalTone; } // LIGHTS if (activeTonesDirty || selectedTone != lastSelectedTone) { for (int i = 0; i < NUM_TONES; i++) { float green = 0.f; float blue = 0.f; float yellow = 0.f; if (state[i]) { if (i==selectedTone) { blue = 0.9f; } else { if (i >= startTone && i <= endTone) { green = 0.9f; // active tone but not selected } else { green = 0.1f; // active but in inactive octave } } } else { if (candidate[i]) { if (i >= startTone && i <= endTone) { yellow = 0.3f; // candidate } else { yellow = 0.1f; // candidate but in inactive octave } } } lights[i * 3].setBrightness(green); lights[i * 3 + 1].setBrightness(blue); lights[i * 3 + 2].setBrightness(yellow); } } activeTonesDirty = false; // only reset after check for lights has been done lastSelectedTone = selectedTone; // OUTPUT outputs[OUT_OUTPUT].value = output; outputs[CHANGEGATE_OUTPUT].value = (changePulse.process(1.0f / engineGetSampleRate()) ? 10.0f : 0.0f); } struct CustomscalerWidget : ModuleWidget { // generate controls const int yStart = 25; const int yRange = 40; const int ySeparator = 5; const float x = 11.5f; const float x2 = 46.5f; const float lastY = 329; // static const float wKnob = 30.23437f; const float wInput = 31.58030f; const float wSwitch = 17.94267f; const float offsetKnob = -2.1; const float offsetSwitch = (wInput - wSwitch) / 2.0f - 1.5; // no idea why 1.5, not centered otherwise const int offsetTL1005 = 4; const bool whiteKey[12] = {true, false, true, false, true, true, false, true, false, true, false, true}; CustomscalerWidget(Customscaler *module) : ModuleWidget(module) { setPanel(SVG::load(assetPlugin(plugin, "res/Customscaler.svg"))); addChild(Widget::create(Vec(15, 0))); addChild(Widget::create(Vec(15, 365))); addChild(Widget::create(Vec(box.size.x-30, 0))); addChild(Widget::create(Vec(box.size.x-30, 365))); // upper panel addInput(Port::create(Vec(x, yStart + 0 * yRange + 0 * ySeparator), Port::INPUT, module, Customscaler::SIGNAL_INPUT)); addParam(ParamWidget::create(Vec(x2 + offsetSwitch, yStart + 0 * yRange + 0 * ySeparator), module, Customscaler::RANGE_PARAM, 0.f, 2.f, 0.f)); addInput(Port::create(Vec(x, yStart + 1 * yRange + 1 * ySeparator), Port::INPUT, module, Customscaler::BASE_INPUT)); addParam(ParamWidget::create(Vec(x2 + offsetKnob, yStart + 1 * yRange + 1 * ySeparator + offsetKnob), module, Customscaler::BASE_PARAM, 0.f, 11.f, 0.f)); addOutput(Port::create(Vec(x, yStart + 2 * yRange + 2 * ySeparator), Port::OUTPUT, module, Customscaler::OUT_OUTPUT)); addOutput(Port::create(Vec(x2, yStart + 2 * yRange + 2 * ySeparator), Port::OUTPUT, module, Customscaler::CHANGEGATE_OUTPUT)); // lower panel addInput(Port::create(Vec(x, lastY - (3 * yRange + 2 * ySeparator)), Port::INPUT, module, Customscaler::TONE_INPUT)); addInput(Port::create(Vec(x2, lastY - (3 * yRange + 2 * ySeparator)), Port::INPUT, module, Customscaler::TOGGLE_TRIGGER_INPUT)); // addInput(Port::create(Vec(x, lastY - (1 * yRange + 1 * ySeparator)), Port::INPUT, module, Customscaler::RANDOM_SUBSET_TRIGGER_INPUT)); // addParam(ParamWidget::create(Vec(x2 + offsetTL1005, lastY - (3 * yRange + 1 * ySeparator - offsetTL1005)), module, Customscaler::RANDOMIZE_BUTTON_PARAM, 0.0f, 1.0f, 0.0f)); addParam(ParamWidget::create(Vec(x2 + offsetKnob, lastY - (2 * yRange + 1 * ySeparator - offsetKnob)), module, Customscaler::P_PARAM, 0.f, 1.f, 0.5f)); addInput(Port::create(Vec(x, lastY - (2 * yRange + 1 * ySeparator)), Port::INPUT, module, Customscaler::P_INPUT)); addInput(Port::create(Vec(x, lastY - (1 * yRange + 1 * ySeparator)), Port::INPUT, module, Customscaler::RANDOMIZE_TRIGGER_INPUT)); addParam(ParamWidget::create(Vec(x2 + offsetSwitch, lastY - (1 * yRange + 1 * ySeparator)), module, Customscaler::MODE_PARAM, 0.f, 1.f, 1.f)); addInput(Port::create(Vec(x, lastY), Port::INPUT, module, Customscaler::RESET_TRIGGER_INPUT)); addParam(ParamWidget::create(Vec(x2 + offsetTL1005, lastY + offsetTL1005), module, Customscaler::RESET_BUTTON_PARAM, 0.0f, 1.0f, 0.0f)); // generate lights float offsetX = mm2px(Vec(17.32, 18.915)).x - mm2px(Vec(16.57, 18.165)).x; // from Mutes float offsetY = mm2px(Vec(17.32, 18.915)).y - mm2px(Vec(16.57, 18.165)).y; for (int octave=0; octave(Vec(x, y), module, Customscaler::TONE1_PARAM + index, 0.0f, 1.0f, 0.0f)); addChild(ModuleLightWidget::create>(Vec(x + offsetX, y + offsetY), module, Customscaler::TONE1_PARAM + index * 3)); /* if (whiteKey[tone]) { addParam(ParamWidget::create(Vec(x, y), module, Customscaler::TONE1_PARAM + index, 0.0f, 1.0f, 0.0f)); addChild(ModuleLightWidget::create>(Vec(x + offsetX, y + offsetY), module, Customscaler::TONE1_PARAM + index * 3)); } else { addParam(ParamWidget::create(Vec(x, y), module, Customscaler::TONE1_PARAM + index, 0.0f, 1.0f, 0.0f)); addChild(ModuleLightWidget::create>(Vec(x + offsetX, y + offsetY), module, Customscaler::TONE1_PARAM + index * 3)); } */ } } }; void appendContextMenu(Menu *menu) override { Customscaler *customscaler = dynamic_cast(module); assert(customscaler); struct UniBiItem : MenuItem { Customscaler *customscaler; void onAction(EventAction &e) override { customscaler->bipolarInput ^= true;; } void step() override { rightText = customscaler->bipolarInput ? "-5V..5V" : "0V..10V"; MenuItem::step(); } }; menu->addChild(construct()); menu->addChild(construct(&MenuItem::text, "Signal input", &UniBiItem::customscaler, customscaler)); }; }; } // namespace rack_plugin_noobhour using namespace rack_plugin_noobhour; RACK_PLUGIN_MODEL_INIT(noobhour, Customscaler) { Model *modelCustomscaler = Model::create("noobhour", "customscale", "Customscaler", QUANTIZER_TAG, RANDOM_TAG); return modelCustomscaler; }