#include "AH.hpp" #include "Core.hpp" #include "UI.hpp" #include "componentlibrary.hpp" #include "dsp/digital.hpp" #include namespace rack_plugin_AmalgamatedHarmonics { struct Sequence { int pDir = 0; int sDir = 0; int nStep = 0; int nDist = 0; int stepI = 0; int cycleI = 0; int stepsRemaining = 0; int cycleRemaining = 0; int currDist = 0; void advanceSequence() { stepI++; stepsRemaining--; } void advanceCycle() { cycleI++; cycleRemaining--; } void initSequence(int inputStep, int inputDist, int inputPDir, int inputSDir, bool locked) { if (!locked) { nStep = inputStep; nDist = inputDist; pDir = inputPDir; sDir = inputSDir; } stepsRemaining = nStep; stepI = 0; // At the beginning of the sequence (i.e. dist = 0) // currDist is the distance from the base note of the sequence, nDist controls the size of the increment currDist = 0; } void setCycle(int n) { cycleRemaining = n; cycleI = 0; } bool isCycleFinished() { return (cycleRemaining == 0); } bool isSequenceFinished() { return (stepsRemaining == 0); } bool isSequenceStarted() { return stepI; } }; struct Arpeggiator : AHModule { const static int MAX_STEPS = 16; const static int MAX_DIST = 12; //Octave const static int NUM_PITCHES = 6; enum ParamIds { STEP_PARAM, DIST_PARAM, PDIR_PARAM, SDIR_PARAM, LOCK_PARAM, TRIGGER_PARAM, NUM_PARAMS }; enum InputIds { CLOCK_INPUT, STEP_INPUT, DIST_INPUT, TRIG_INPUT, PITCH_INPUT, NUM_INPUTS = PITCH_INPUT + NUM_PITCHES }; enum OutputIds { OUT_OUTPUT, GATE_OUTPUT, EOC_OUTPUT, EOS_OUTPUT, NUM_OUTPUTS }; enum LightIds { LOCK_LIGHT, NUM_LIGHTS }; Arpeggiator() : AHModule(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) { } void step() override; SchmittTrigger clockTrigger; // for clock SchmittTrigger trigTrigger; // for step trigger SchmittTrigger lockTrigger; SchmittTrigger buttonTrigger; PulseGenerator triggerPulse; PulseGenerator gatePulse; PulseGenerator eosPulse; PulseGenerator eocPulse; float pitches[NUM_PITCHES]; float inputPitches[NUM_PITCHES]; bool pitchStatus[NUM_PITCHES]; float pitchValue[NUM_PITCHES]; int inputPDir; int inputSDir; int inputStep = 0; int inputDist = 0; bool locked = false; float outVolts; bool isRunning = false; bool freeRunning = false; Sequence seq; int newSequence = 0; int newCycle = 0; const static int LAUNCH = 1; const static int COUNTDOWN = 3; int nValidPitches = 0; int poll = 5000; }; void Arpeggiator::step() { stepX++; // Wait a few steps for the inputs to flow through Rack if (stepX < 10) { return; } // Get the clock rate and semi-tone float semiTone = 1.0 / 12.0; // Get inputs from Rack float clockInput = inputs[CLOCK_INPUT].value; float trigInput = inputs[TRIG_INPUT].value; float trigActive = inputs[TRIG_INPUT].active; float lockInput = params[LOCK_PARAM].value; float buttonInput = params[TRIGGER_PARAM].value; float iPDir = params[PDIR_PARAM].value; float iSDir = params[SDIR_PARAM].value; float iStep; if (inputs[STEP_INPUT].active) { iStep = inputs[STEP_INPUT].value; inputStep = round(rescale(iStep, -10.0f, 10.0f, 0.0f, MAX_STEPS)); } else { iStep = params[STEP_PARAM].value; inputStep = iStep; } float iDist; if (inputs[DIST_INPUT].active) { iDist = inputs[DIST_INPUT].value; inputDist = round(rescale(iDist, -10.0f, 10.0f, 0.0f, MAX_DIST)); } else { iDist = params[DIST_PARAM].value; inputDist = iDist; } for (int p = 0; p < NUM_PITCHES; p++) { int index = PITCH_INPUT + p; pitchStatus[p] = inputs[index].active; pitchValue[p] = inputs[index].value; } // Process inputs bool clockStatus = clockTrigger.process(clockInput); bool triggerStatus = trigTrigger.process(trigInput); bool lockStatus = lockTrigger.process(lockInput); bool buttonStatus = buttonTrigger.process(buttonInput); inputPDir = iPDir; inputSDir = iSDir; int nValidPitches = 0; for (int p = 0; p < NUM_PITCHES; p++) { if (pitchStatus[p]) { //Plugged in inputPitches[nValidPitches] = pitchValue[p]; nValidPitches++; } } // Check that we even have anything plugged in if (nValidPitches == 0) { return; // No inputs, no music } // Has the trigger input been fired if (triggerStatus) { triggerPulse.trigger(5e-5); if (debugEnabled()) { std::cout << stepX << " Triggered" << std::endl; } } // Update the trigger pulse and determine if it is still high bool triggerHigh = triggerPulse.process(delta); if (debugEnabled()) { if (triggerHigh) { std::cout << stepX << " Trigger is high" << std::endl; } } // Update lock if (lockStatus) { if (debugEnabled()) { std::cout << "Toggling lock: " << locked << std::endl; } locked = !locked; } if (newSequence) { newSequence--; if (debugEnabled()) { std::cout << stepX << " Countdown newSequence " << newSequence << std::endl; } } if (newCycle) { newCycle--; if (debugEnabled()) { std::cout << stepX << " Countdown newCycle " << newCycle << std::endl; } } // OK so the problem here might be that the clock gate is still high right after the trigger gate fired on the previous step // So we need to wait a while for the clock gate to go low // Has the clock input been fired bool isClocked = false; if (clockStatus && !triggerHigh) { if (debugEnabled()) { std::cout << stepX << " Clocked" << std::endl; } isClocked = true; } // Has the trigger input been fired, either on the input or button if (triggerStatus || buttonStatus) { newSequence = COUNTDOWN; newCycle = COUNTDOWN; if (debugEnabled()) { std::cout << stepX << " Triggered" << std::endl; } } // So this is where the free-running could be triggered if (isClocked && !isRunning) { // Must have a clock and not be already running if (!trigActive) { // If nothing plugged into the TRIG input if (debugEnabled()) { std::cout << stepX << " Free running sequence; starting" << std::endl; } freeRunning = true; // We're free-running newSequence = COUNTDOWN; newCycle = LAUNCH; } else { if (debugEnabled()) { std::cout << stepX << " Triggered sequence; wait for trigger" << std::endl; } freeRunning = false; } } // Detect cable being plugged in when free-running, stop free-running if (freeRunning && trigActive && isRunning) { if (debugEnabled()) { std::cout << stepX << " TRIG input re-connected" << std::endl; } freeRunning = false; } // Reached the end of the cycle if (isRunning && isClocked && seq.isCycleFinished()) { // Completed 1 step seq.advanceSequence(); // Pulse the EOC gate eocPulse.trigger(5e-3); if (debugEnabled()) { std::cout << stepX << " Finished Cycle S: " << seq.stepI << " C: " << seq.cycleI << " sRemain: " << seq.stepsRemaining << " cRemain: " << seq.cycleRemaining << std::endl; } // Reached the end of the sequence if (isRunning && seq.isSequenceFinished()) { // Free running, so start new seqeuence & cycle if (freeRunning) { newCycle = COUNTDOWN; newSequence = COUNTDOWN; } isRunning = false; // Pulse the EOS gate eosPulse.trigger(5e-3); if (debugEnabled()) { std::cout << stepX << " Finished sequence S: " << seq.stepI << " C: " << seq.cycleI << " sRemain: " << seq.stepsRemaining << " cRemain: " << seq.cycleRemaining << " flag:" << isRunning << std::endl; } } else { newCycle = LAUNCH; if (debugEnabled()) { std::cout << stepX << " Flagging new cycle" << std::endl; } } } // If we have been triggered, start a new sequence if (newSequence == LAUNCH) { // At the first step of the sequence if (debugEnabled()) { std::cout << stepX << " New Sequence" << std::endl; } if (!locked) { if (debugEnabled()) { std::cout << stepX << " Update sequence inputs" << std::endl; } } // So this is where we tweak the sequence parameters seq.initSequence(inputStep, inputDist, inputPDir, inputSDir, locked); // We're running now isRunning = true; } // Starting a new cycle if (newCycle == LAUNCH) { if (debugEnabled()) { std::cout << stepX << " Defining cycle: nStep: " << seq.nStep << " nDist: " << seq.nDist << " pDir: " << seq.pDir << " sDir: " << seq.sDir << " nValidPitches: " << nValidPitches << " seqLen: " << seq.nStep * nValidPitches; for (int i = 0; i < nValidPitches; i++) { std::cout << " P" << i << " V: " << inputPitches[i]; } std::cout << std::endl; } /// Reset the cycle counters seq.setCycle(nValidPitches); if (debugEnabled()) { std::cout << stepX << " New cycle" << std::endl; } // Deal with RND setting, when sDir == 1, force it up or down if (seq.sDir == 1) { if (rand() % 2 == 0) { seq.sDir = 0; } else { seq.sDir = 2; } } // Only starting moving after the first cycle if (seq.isSequenceStarted()) { switch (seq.sDir) { case 0: seq.currDist--; break; case 2: seq.currDist++; break; default: ; } } if (!locked) {// Pitches are locked, and so is the order. This keeps randomly generated arps fixed when locked // pitches[i] are offset from the input values according to the dist setting. Here we calculate the offsets for (int i = 0; i < nValidPitches; i++) { int target; // Read the pitches according to direction, but we should do this for the sequence? switch (seq.pDir) { case 0: target = nValidPitches - i - 1; break; // DOWN case 1: target = rand() % nValidPitches; break; // RANDOM case 2: target = i; break; // UP default: target = i; break; // For random case, read randomly from array, so order does not matter } // How many semi-tones do we need to shift float dV = semiTone * seq.nDist * seq.currDist; pitches[i] = clamp(inputPitches[target] + dV, -10.0, 10.0); if (debugEnabled()) { std::cout << stepX << " Pitch: " << i << " stepI: " << seq.stepI << " dV:" << dV << " target: " << target << " in: " << inputPitches[target] << " out: " << pitches[target] << std::endl; } } } if (debugEnabled()) { std::cout << stepX << " Output pitches: "; for (int i = 0; i < nValidPitches; i++) { std::cout << " P" << i << " V: " << pitches[i]; } std::cout << std::endl; } } // Advance the sequence // Are we starting a sequence or are running and have been clocked; if so advance the sequence // Only advance from the clock if (isRunning && (isClocked || newCycle == LAUNCH)) { if (debugEnabled()) { std::cout << stepX << " Advance Cycle S: " << seq.stepI << " C: " << seq.cycleI << " sRemain: " << seq.stepsRemaining << " cRemain: " << seq.cycleRemaining << std::endl; } // Finally set the out voltage outVolts = pitches[seq.cycleI]; if (debugEnabled()) { std::cout << stepX << " Output V = " << outVolts << std::endl; } // Update counters seq.advanceCycle(); // Pulse the output gate gatePulse.trigger(5e-4); } // Set the value lights[LOCK_LIGHT].value = locked ? 1.0 : 0.0; outputs[OUT_OUTPUT].value = outVolts; bool gPulse = gatePulse.process(delta); bool sPulse = eosPulse.process(delta); bool cPulse = eocPulse.process(delta); outputs[GATE_OUTPUT].value = gPulse ? 10.0 : 0.0; outputs[EOS_OUTPUT].value = sPulse ? 10.0 : 0.0; outputs[EOC_OUTPUT].value = cPulse ? 10.0 : 0.0; } struct ArpeggiatorDisplay : TransparentWidget { Arpeggiator *module; int frame = 0; std::shared_ptr font; ArpeggiatorDisplay() { font = Font::load(assetPlugin(plugin, "res/Roboto-Light.ttf")); } void draw(NVGcontext *vg) override { Vec pos = Vec(0, 20); nvgFontSize(vg, 20); nvgFontFaceId(vg, font->handle); nvgTextLetterSpacing(vg, -1); nvgFillColor(vg, nvgRGBA(212, 175, 55, 0xff)); char text[128]; snprintf(text, sizeof(text), "STEP: %d [%d]", module->seq.nStep, module->inputStep); nvgText(vg, pos.x + 10, pos.y + 5, text, NULL); snprintf(text, sizeof(text), "DIST: %d [%d]", module->seq.nDist, module->inputDist); nvgText(vg, pos.x + 10, pos.y + 25, text, NULL); if (module->seq.sDir == 0) { snprintf(text, sizeof(text), "SEQ: DSC"); } else { snprintf(text, sizeof(text), "SEQ: ASC"); } nvgText(vg, pos.x + 10, pos.y + 45, text, NULL); switch(module->seq.pDir) { case 0: snprintf(text, sizeof(text), "ARP: R-L"); break; case 1: snprintf(text, sizeof(text), "ARP: RND"); break; case 2: snprintf(text, sizeof(text), "ARP: L-R"); break; default: snprintf(text, sizeof(text), "ARP: ERR"); break; } nvgText(vg, pos.x + 10, pos.y + 65, text, NULL); std::string inputs ("IN: "); for (int p = 0; p < Arpeggiator::NUM_PITCHES; p++) { if (module->pitchStatus[p] && module->pitchValue[p] > -9.999) { //Plugged in or approx -10.0 inputs = inputs + std::to_string(p + 1); } } nvgText(vg, pos.x + 10, pos.y + 85, inputs.c_str(), NULL); } }; struct ArpeggiatorWidget : ModuleWidget { ArpeggiatorWidget(Arpeggiator *module); }; ArpeggiatorWidget::ArpeggiatorWidget(Arpeggiator *module) : ModuleWidget(module) { UI ui; box.size = Vec(240, 380); { SVGPanel *panel = new SVGPanel(); panel->box.size = box.size; panel->setBackground(SVG::load(assetPlugin(plugin, "res/Arpeggiator.svg"))); addChild(panel); } addChild(Widget::create(Vec(15, 0))); addChild(Widget::create(Vec(box.size.x - 30, 0))); addChild(Widget::create(Vec(15, 365))); addChild(Widget::create(Vec(box.size.x - 30, 365))); { ArpeggiatorDisplay *display = new ArpeggiatorDisplay(); display->module = module; display->box.pos = Vec(10, 95); display->box.size = Vec(100, 140); addChild(display); } addOutput(Port::create(ui.getPosition(UI::PORT, 0, 0, false, false), Port::OUTPUT, module, Arpeggiator::OUT_OUTPUT)); addOutput(Port::create(ui.getPosition(UI::PORT, 1, 0, false, false), Port::OUTPUT, module, Arpeggiator::GATE_OUTPUT)); addParam(ParamWidget::create(ui.getPosition(UI::BUTTON, 2, 0, false, false), module, Arpeggiator::LOCK_PARAM, 0.0, 1.0, 0.0)); addChild(ModuleLightWidget::create>(ui.getPosition(UI::LIGHT, 2, 0, false, false), module, Arpeggiator::LOCK_LIGHT)); addOutput(Port::create(ui.getPosition(UI::PORT, 3, 0, false, false), Port::OUTPUT, module, Arpeggiator::EOC_OUTPUT)); addOutput(Port::create(ui.getPosition(UI::PORT, 4, 0, false, false), Port::OUTPUT, module, Arpeggiator::EOS_OUTPUT)); addParam(ParamWidget::create(Vec(127, 155), module, Arpeggiator::TRIGGER_PARAM, 0.0, 1.0, 0.0)); for (int i = 0; i < Arpeggiator::NUM_PITCHES; i++) { addInput(Port::create(ui.getPosition(UI::PORT, i, 5, true, false), Port::INPUT, module, Arpeggiator::PITCH_INPUT + i)); } addInput(Port::create(ui.getPosition(UI::PORT, 0, 4, true, false), Port::INPUT, module, Arpeggiator::STEP_INPUT)); addParam(ParamWidget::create(ui.getPosition(UI::KNOB, 1, 4, true, false), module, Arpeggiator::STEP_PARAM, 1.0, 16.0, 1.0)); addInput(Port::create(ui.getPosition(UI::PORT, 2, 4, true, false), Port::INPUT, module, Arpeggiator::DIST_INPUT)); addParam(ParamWidget::create(ui.getPosition(UI::KNOB, 3, 4, true, false), module, Arpeggiator::DIST_PARAM, 0.0, 12.0, 0.0)); addInput(Port::create(ui.getPosition(UI::PORT, 4, 4, true, false), Port::INPUT, module, Arpeggiator::TRIG_INPUT)); addInput(Port::create(ui.getPosition(UI::PORT, 5, 4, true, false), Port::INPUT, module, Arpeggiator::CLOCK_INPUT)); addParam(ParamWidget::create(Vec(178.5, 112.0), module, Arpeggiator::SDIR_PARAM, 0, 2, 0)); addParam(ParamWidget::create(Vec(178.5, 187.0), module, Arpeggiator::PDIR_PARAM, 0, 2, 0)); } } // namespace rack_plugin_AmalgamatedHarmonics using namespace rack_plugin_AmalgamatedHarmonics; RACK_PLUGIN_MODEL_INIT(AmalgamatedHarmonics, Arpeggiator) { Model *modelArpeggiator = Model::create( "Amalgamated Harmonics", "Arpeggiator", "Arpeggiator (deprecated)", ARPEGGIATOR_TAG); return modelArpeggiator; }