#include #include #include #include "alikins.hpp" #include "dsp/digital.hpp" // #include "util.hpp" namespace rack_plugin_Alikins { /* IdleSwitch * * What: * * If no input events are seen at Input Source within the timeout period * emit a gate on Idle Gate Output that lasts until there are input events * again. Then reset the timeout period. * * Sort of metaphoricaly like an idle handler or timeout in event based * programming like GUI main loops. * * The timeout period is set by the value * of the 'Time before idle' param. * * If there is a 'Reset idle' source, when it gets an event, the timeout period * is reset. After a reset event, the Idle Gate Output will remain on until * an input event is seen at Input Source. When there is an input event, the Idle * Gate Output is turned off until the expiration of the 'Time before idle' or * the next 'Reset idle'. * * To use the eventloop/gui main loop analogy, a 'Reset idle' event is equilivent to * running an idle handler directly (or running a mainloop iteration with no non-idle * events pending). * * Why: * * Original intentional was to use in combo with a human player and midi/cv keyboard. * As long as the human is playing, the IdleSwitch output is 'off', but if they go * idle for some time period the output is turned on. For example, a patch may plain * loud drone when idle, but would turn the drone off or down when the human played * and then turn it back on when it stopped. Or maybe it could be used to start an * drum fill... * * The 'Reset idle' input allows this be kind of synced to a clock, beat, or sequence. * In the dronevexample above, the drone would then only come back in on a beat. * * And perhaps most importantly, it can be used to do almost random output and * make weird noises. */ /* TODO * - is there a 'standard' for communicating lengths of time (like delay time)? * - idle start trigger * - idle end trigger * - switch for output to be high for idle or low for idle * - time display widget for timeout length * - Fine/Course params fors for timeout * - idle timeout countdown display for remaining time before timeout * - gui 'progress' widget? */ struct IdleSwitch : Module { enum ParamIds { TIME_PARAM, NUM_PARAMS }; enum InputIds { INPUT_SOURCE_INPUT, HEARTBEAT_INPUT, TIME_INPUT, PULSE_INPUT, SWITCHED_INPUT, NUM_INPUTS }; enum OutputIds { IDLE_GATE_OUTPUT, TIME_OUTPUT, IDLE_START_OUTPUT, IDLE_END_OUTPUT, FRAME_COUNT_OUTPUT, ON_WHEN_IDLE_OUTPUT, OFF_WHEN_IDLE_OUTPUT, NUM_OUTPUTS }; enum LightIds { NUM_LIGHTS }; int idleTimeoutMS = 140; int idleTimeLeftMS = 0; SchmittTrigger inputTrigger; // FIXME: these names are confusing SchmittTrigger heartbeatTrigger; // clock mode stuff SchmittTrigger pulseTrigger; int pulseFrame = 0; bool waiting_for_pulse = false; bool pulse_mode = false; PulseGenerator idleStartPulse; PulseGenerator idleEndPulse; // FIXME: not really counts int frameCount = 0; int maxFrameCount = 0; float idleGateOutput = 0.0; float deltaTime = 0; bool is_idle = false; IdleSwitch() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) {} void step() override; }; void IdleSwitch::step() { bool pulse_seen = false; bool time_exceeded = false; pulse_mode = inputs[PULSE_INPUT].active; float sampleRate = engineGetSampleRate(); // Compute the length of our idle time based on the knob + time cv // -or- // base it one the time since the last clock pulse if (pulse_mode) { if (inputTrigger.process(inputs[PULSE_INPUT].value)) { // keep track of which frame we got a pulse // FIXME: without a max time, frameCount can wrap? // update pulseFrame to point to current frame count pulseFrame = frameCount; waiting_for_pulse = true; pulse_seen = true; } deltaTime = fmax(frameCount - pulseFrame, 0) / sampleRate; // if we are waiting, maxframeCount is the time since last pulse and increasing maxFrameCount = frameCount; } else { deltaTime = params[TIME_PARAM].value; if (inputs[TIME_INPUT].active) { deltaTime += clamp(inputs[TIME_INPUT].value, 0.0f, 10.0f); } // TODO: refactor into submethods if not subclass maxFrameCount = (int)ceilf(deltaTime * sampleRate); } idleTimeoutMS = std::round(deltaTime*1000); // debug("is_idle: %d pulse_mode: %d pulse_frame: %d frameCount: %d maxFrameCount: %d ", is_idle, pulse_mode, pulseFrame, frameCount, maxFrameCount); // debug("is_idle: %d pulse_mode: %d w_f_pulse: %d pulse_seen: %d pulseFrame: %d frameCount: %d deltaTime: %f", // is_idle, pulse_mode, waiting_for_pulse, pulse_seen, pulseFrame, frameCount, deltaTime); if (inputs[HEARTBEAT_INPUT].active && heartbeatTrigger.process(inputs[HEARTBEAT_INPUT].value)) { frameCount = 0; } // time_left_s is always 0 for pulse mode until we predict the future float frames_left = fmax(maxFrameCount - frameCount, 0); float time_left_s = frames_left / sampleRate; // TODO: simplify the start/end/gate on logic... really only a few states to check // the start of idle (not idle -> idle trans) if ((frameCount > maxFrameCount) || (waiting_for_pulse && pulse_seen)) { time_exceeded = true; if (!is_idle) { idleStartPulse.trigger(0.01); } } // stay idle once we start until there is an input event is_idle = (is_idle || time_exceeded); if (is_idle) { idleGateOutput = 10.0; outputs[ON_WHEN_IDLE_OUTPUT].value = inputs[SWITCHED_INPUT].value; outputs[OFF_WHEN_IDLE_OUTPUT].value = 0.0f; } else { idleGateOutput = 0.0; outputs[ON_WHEN_IDLE_OUTPUT].value = 0.0f; outputs[OFF_WHEN_IDLE_OUTPUT].value = inputs[SWITCHED_INPUT].value; is_idle = false; // if we arent idle yet, the idleTimeLeft is changing and we need to update time remaining display // update idletimeLeftMS which drives the digit display widget idleTimeLeftMS = time_left_s*1000; } frameCount++; if (inputs[INPUT_SOURCE_INPUT].active && inputTrigger.process(inputs[INPUT_SOURCE_INPUT].value)) { // only end idle if we are already idle (idle->not idle transition) if (is_idle) { idleEndPulse.trigger(0.01); } is_idle = false; waiting_for_pulse = false; frameCount = 0; pulseFrame = 0; } // once clock input works, could add an output to indicate how long between clock // If in pulse mode, deltaTime can be larger than 10s internal, but the max output // to "Time output" is 10V. ie, after 10s the "Time Output" stops increasing. outputs[TIME_OUTPUT].value = clamp(deltaTime, 0.0f, 10.0f); outputs[IDLE_GATE_OUTPUT].value = idleGateOutput; outputs[IDLE_START_OUTPUT].value = idleStartPulse.process(1.0/engineGetSampleRate()) ? 10.0 : 0.0; outputs[IDLE_END_OUTPUT].value = idleEndPulse.process(1.0/engineGetSampleRate()) ? 10.0 : 0.0; } // From AS DelayPlus.cpp https://github.com/AScustomWorks/AS struct IdleSwitchMsDisplayWidget : TransparentWidget { int *value; std::shared_ptr font; IdleSwitchMsDisplayWidget() { font = Font::load(assetPlugin(plugin, "res/Segment7Standard.ttf")); } void draw(NVGcontext *vg) override { // Background // these go to... NVGcolor backgroundColor = nvgRGB(0x11, 0x11, 0x11); NVGcolor borderColor = nvgRGB(0xff, 0xff, 0xff); nvgBeginPath(vg); nvgRoundedRect(vg, 0.0, 0.0, box.size.x, box.size.y, 5.0); nvgFillColor(vg, backgroundColor); nvgFill(vg); nvgStrokeWidth(vg, 3.0); nvgStrokeColor(vg, borderColor); nvgStroke(vg); // text nvgFontSize(vg, 18); nvgFontFaceId(vg, font->handle); nvgTextLetterSpacing(vg, 2.5); std::stringstream to_display; to_display << std::right << std::setw(5) << *value; Vec textPos = Vec(0.5f, 19.0f); NVGcolor textColor = nvgRGB(0x65, 0xf6, 0x78); nvgFillColor(vg, textColor); nvgText(vg, textPos.x, textPos.y, to_display.str().c_str(), NULL); } }; struct IdleSwitchWidget : ModuleWidget { IdleSwitchWidget(IdleSwitch *module); }; IdleSwitchWidget::IdleSwitchWidget(IdleSwitch *module) : ModuleWidget(module) { setPanel(SVG::load(assetPlugin(plugin, "res/IdleSwitch.svg"))); addChild(Widget::create(Vec(5, 0))); addChild(Widget::create(Vec(box.size.x - 20, 365))); addInput(Port::create(Vec(37, 20.0), Port::INPUT, module, IdleSwitch::INPUT_SOURCE_INPUT)); addInput(Port::create(Vec(37, 60.0), Port::INPUT, module, IdleSwitch::HEARTBEAT_INPUT)); addInput(Port::create(Vec(70, 60.0), Port::INPUT, module, IdleSwitch::PULSE_INPUT)); // idle time display // FIXME: handle large IdleTimeoutMs (> 99999ms) better IdleSwitchMsDisplayWidget *idle_time_display = new IdleSwitchMsDisplayWidget(); idle_time_display->box.pos = Vec(20, 115); idle_time_display->box.size = Vec(70, 24); idle_time_display->value = &module->idleTimeoutMS; addChild(idle_time_display); addInput(Port::create(Vec(10, 155.0), Port::INPUT, module, IdleSwitch::TIME_INPUT)); addParam(ParamWidget::create(Vec(38.86, 150.0), module, IdleSwitch::TIME_PARAM, 0.0, 10.0, 0.25)); addOutput(Port::create(Vec(80, 155.0), Port::OUTPUT, module, IdleSwitch::TIME_OUTPUT)); IdleSwitchMsDisplayWidget *time_remaining_display = new IdleSwitchMsDisplayWidget(); time_remaining_display->box.pos = Vec(20, 225); time_remaining_display->box.size = Vec(70, 24); time_remaining_display->value = &module->idleTimeLeftMS; addChild(time_remaining_display); addOutput(Port::create(Vec(10, 263.0), Port::OUTPUT, module, IdleSwitch::IDLE_START_OUTPUT)); addOutput(Port::create(Vec(47.5, 263.0), Port::OUTPUT, module, IdleSwitch::IDLE_GATE_OUTPUT)); addOutput(Port::create(Vec(85, 263.0), Port::OUTPUT, module, IdleSwitch::IDLE_END_OUTPUT)); addInput(Port::create(Vec(10.0f, 315.0f), Port::INPUT, module, IdleSwitch::SWITCHED_INPUT)); addOutput(Port::create(Vec(47.5f, 315.0f), Port::OUTPUT, module, IdleSwitch::ON_WHEN_IDLE_OUTPUT)); addOutput(Port::create(Vec(85.0f, 315.0f), Port::OUTPUT, module, IdleSwitch::OFF_WHEN_IDLE_OUTPUT)); } } // namespace rack_plugin_Alikins using namespace rack_plugin_Alikins; RACK_PLUGIN_MODEL_INIT(Alikins, IdleSwitch) { Model *modelIdleSwitch = Model::create( "Alikins", "IdleSwitch", "Idle Switch", SWITCH_TAG , UTILITY_TAG); return modelIdleSwitch; }