#include "AH.hpp" #include "Core.hpp" #include "UI.hpp" #include "dsp/digital.hpp" #include namespace rack_plugin_AmalgamatedHarmonics { struct BombeChord { int rootNote; int quality; int chord; int modeDegree; int inversion; float outVolts[6]; BombeChord() : rootNote(0), quality(0), chord(0), modeDegree(0), inversion(0) { for (int j = 0; j < 6; j++) { outVolts[j] = 0.0f; } } }; struct Bombe : AHModule { const static int NUM_PITCHES = 6; const static int N_NOTES = 12; const static int N_DEGREES = 7; const static int N_QUALITIES = 6; const static int QMAP_SIZE = 20; enum ParamIds { KEY_PARAM, MODE_PARAM, LENGTH_PARAM, X_PARAM, Y_PARAM, NUM_PARAMS }; enum InputIds { CLOCK_INPUT, KEY_INPUT, MODE_INPUT, FREEZE_INPUT, Y_INPUT, NUM_INPUTS }; enum OutputIds { ENUMS(PITCH_OUTPUT,6), NUM_OUTPUTS }; enum LightIds { ENUMS(LOCK_LIGHT,2), NUM_LIGHTS }; Bombe() : AHModule(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) { for(int i = 0; i < BUFFERSIZE; i++) { buffer[i].chord = 1; int *chordArray = CoreUtil().ChordTable[buffer[i].chord].root; for (int j = 0; j < 6; j++) { buffer[i].outVolts[j] = CoreUtil().getVoltsFromPitch(chordArray[j], buffer[i].rootNote); } } } void step() override; void modeRandom(BombeChord lastValue, float y); void modeSimple(BombeChord lastValue, float y); void modeKey(BombeChord lastValue, float y); void modeGalaxy(BombeChord lastValue, float y); void modeComplex(BombeChord lastValue, float y); json_t *toJson() override { json_t *rootJ = json_object(); // offset json_t *offsetJ = json_integer((int) offset); json_object_set_new(rootJ, "offset", offsetJ); // mode json_t *modeJ = json_integer((int) mode); json_object_set_new(rootJ, "mode", modeJ); // inversions json_t *inversionsJ = json_integer((int) allowedInversions); json_object_set_new(rootJ, "inversions", inversionsJ); return rootJ; } void fromJson(json_t *rootJ) override { // offset json_t *offsetJ = json_object_get(rootJ, "offset"); if (offsetJ) offset = json_integer_value(offsetJ); // mode json_t *modeJ = json_object_get(rootJ, "mode"); if (modeJ) mode = json_integer_value(modeJ); // mode json_t *inversionsJ = json_object_get(rootJ, "inversions"); if (inversionsJ) allowedInversions = json_integer_value(inversionsJ); } Core core; const static int N_CHORDS = 98; int ChordMap[N_CHORDS] = {1,2,26,29,71,28,72,91,31,97,25,44,54,61,78,95,10,14,15,17,48,79,81,85,11,30,89,94,24,3,90,98,96,60,55,86,5,93,7,56,92,16,32,46,62,77,18,49,65,68,70,82,20,22,23,45,83,87,6,21,27,42,80,9,52,69,76,13,37,88,53,58,8,41,57,47,64,73,19,50,59,66,74,12,35,38,63,33,34,51,4,36,40,43,84,67,39,75}; int MajorScale[7] = {0,2,4,5,7,9,11}; int Quality2Chord[N_QUALITIES] = { 1, 71, 91 }; // M, m, dim int QualityMap[3][QMAP_SIZE] = { {01,01,01,01,01,01,01,01,01,01,25,25,25,25,25,25,25,25,31,31}, {71,71,71,71,71,71,71,71,71,71,78,78,78,78,78,78,78,78,31,31}, {91,91,91,91,91,91,91,91,91,91,94,94,94,94,94,94,94,94,94,94} }; int InversionMap[3][QMAP_SIZE] = { {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1}, {0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,2,2}, }; int poll = 50000; SchmittTrigger clockTrigger; int currRoot = 1; int currMode = 1; int currInversion = 0; int length = 16; int offset = 12; // 0 = random, 12 = lower octave, 24 = repeat, 36 = upper octave int mode = 1; // 0 = random chord, 1 = chord in key, 2 = chord in mode int allowedInversions = 0; // 0 = root only, 1 = root + first, 2 = root, first, second std::string rootName = ""; std::string modeName = ""; const static int BUFFERSIZE = 16; BombeChord buffer[BUFFERSIZE]; BombeChord displayBuffer[BUFFERSIZE]; }; void Bombe::modeSimple(BombeChord lastValue, float y) { // Recalculate new value of buffer[0].outVolts from lastValue int shift = (rand() % (N_DEGREES - 1)) + 1; // 1 - 6 - always new chord buffer[0].modeDegree = (lastValue.modeDegree + shift) % N_DEGREES; // FIXME, come from mode2 modeDeg == -1! int q; int n; CoreUtil().getRootFromMode(currMode,currRoot,buffer[0].modeDegree,&n,&q); buffer[0].rootNote = n; buffer[0].quality = q; // 0 = Maj, 1 = Min, 2 = Dim if (randomUniform() < y) { buffer[0].chord = QualityMap[q][rand() % QMAP_SIZE]; // Get the index into the main chord table } else { buffer[0].chord = Quality2Chord[q]; // Get the index into the main chord table } buffer[0].inversion = InversionMap[allowedInversions][rand() % QMAP_SIZE]; } void Bombe::modeRandom(BombeChord lastValue, float y) { // Recalculate new value of buffer[0].outVolts from lastValue float p = randomUniform(); if (p < y) { buffer[0].rootNote = rand() % 12; } else { buffer[0].rootNote = MajorScale[rand() % 7]; } buffer[0].modeDegree = -1; // FIXME buffer[0].quality = -1; // FIXME int maxChord = (int)((float)N_CHORDS * y) + 1; buffer[0].chord = ChordMap[rand() % maxChord]; buffer[0].inversion = InversionMap[allowedInversions][rand() % QMAP_SIZE]; } void Bombe::modeKey(BombeChord lastValue, float y) { int shift = (rand() % (N_DEGREES - 1)) + 1; // 1 - 6 - always new chord buffer[0].modeDegree = (lastValue.modeDegree + shift) % N_DEGREES; // FIXME, come from mode2 modeDeg == -1! int q; int n; CoreUtil().getRootFromMode(currMode,currRoot,buffer[0].modeDegree,&n,&q); buffer[0].rootNote = n; buffer[0].quality = -1; // FIXME buffer[0].chord = (rand() % (CoreUtil().NUM_CHORDS - 1)) + 1; // Get the index into the main chord table buffer[0].inversion = InversionMap[allowedInversions][rand() % QMAP_SIZE]; } void Bombe::modeGalaxy(BombeChord lastValue, float y) { float excess = y - randomUniform(); if (excess < 0.0) { modeSimple(lastValue, y); } else { if (excess < 0.2) { modeKey(lastValue, y); } else { modeRandom(lastValue, y); } } } void Bombe::modeComplex(BombeChord lastValue, float y) { modeGalaxy(lastValue, y); // Default to Galaxy } void Bombe::step() { AHModule::step(); // Get inputs from Rack bool clocked = clockTrigger.process(inputs[CLOCK_INPUT].value); bool locked = false; bool updated = false; if (inputs[MODE_INPUT].active) { float fMode = inputs[MODE_INPUT].value; currMode = CoreUtil().getModeFromVolts(fMode); } else { currMode = params[MODE_PARAM].value; } if (inputs[KEY_INPUT].active) { float fRoot = inputs[KEY_INPUT].value; currRoot = CoreUtil().getKeyFromVolts(fRoot); } else { currRoot = params[KEY_PARAM].value; } float x = params[X_PARAM].value; float y = clamp(params[Y_PARAM].value + inputs[Y_PARAM].value * 0.1f, 0.0, 1.0); length = params[LENGTH_PARAM].value; if ((x >= 1.0f) || (inputs[FREEZE_INPUT].value > 0.000001f)) { locked = true; } switch(mode) { case 0: // Random rootName = ""; modeName = ""; break; case 1: // Simple rootName = CoreUtil().noteNames[currRoot]; modeName = CoreUtil().modeNames[currMode]; break; case 2: // Galaxy rootName = CoreUtil().noteNames[currRoot]; modeName = CoreUtil().modeNames[currMode]; break; case 3: // Complex rootName = CoreUtil().noteNames[currRoot]; modeName = CoreUtil().modeNames[currMode]; break; default: rootName = ""; modeName = ""; } if (clocked) { // Grab value from last element of sub-array, which will be the new head value BombeChord lastValue = buffer[length - 1]; // Shift buffer for(int i = length - 1; i > 0; i--) { buffer[i] = buffer[i-1]; } // Set first element if (locked) { // Buffer is locked buffer[0] = lastValue; } else { if (randomUniform() < x) { // Buffer update skipped buffer[0] = lastValue; } else { // We are going to update this entry updated = true; switch(mode) { case 0: modeRandom(lastValue, y); break; case 1: modeSimple(lastValue, y); break; case 2: modeGalaxy(lastValue, y); break; case 3: modeComplex(lastValue, y); break; default: modeSimple(lastValue, y); } // Determine which chord corresponds to the grid position int *chordArray; switch(buffer[0].inversion) { case 0: chordArray = CoreUtil().ChordTable[buffer[0].chord].root; break; case 1: chordArray = CoreUtil().ChordTable[buffer[0].chord].first; break; case 2: chordArray = CoreUtil().ChordTable[buffer[0].chord].second; break; default: chordArray = CoreUtil().ChordTable[buffer[0].chord].root; } // Determine which notes corresponds to the chord for (int j = 0; j < NUM_PITCHES; j++) { if (chordArray[j] < 0) { int off = offset; if (offset == 0) { // if offset = 0, randomise offset per note off = (rand() % 3 + 1) * 12; } buffer[0].outVolts[j] = CoreUtil().getVoltsFromPitch(chordArray[j] + off, buffer[0].rootNote); } else { buffer[0].outVolts[j] = CoreUtil().getVoltsFromPitch(chordArray[j], buffer[0].rootNote); } } } } for(int i = BUFFERSIZE - 1; i > 0; i--) { displayBuffer[i] = displayBuffer[i-1]; } displayBuffer[0] = buffer[0]; // for(int i = 0; i < BUFFERSIZE; i++) { // std::cout << buffer[i].rootNote; // } // std::cout << std::endl; // for(int i = 0; i < BUFFERSIZE; i++) { // std::cout << displayBuffer[i].rootNote; // } // std::cout << std::endl << std::endl; } if (updated) { // Green Update lights[LOCK_LIGHT].setBrightnessSmooth(1.0f); lights[LOCK_LIGHT + 1].setBrightnessSmooth(0.0f); } else if (locked) { // Yellow locked lights[LOCK_LIGHT].setBrightnessSmooth(0.0f); lights[LOCK_LIGHT + 1].setBrightnessSmooth(1.0f); } else { // No change lights[LOCK_LIGHT].setBrightnessSmooth(0.0f); lights[LOCK_LIGHT + 1].setBrightnessSmooth(0.0f); } // Set the output pitches and lights for (int i = 0; i < NUM_PITCHES; i++) { outputs[PITCH_OUTPUT + i].value = buffer[0].outVolts[i]; } } struct BombeDisplay : TransparentWidget { Bombe *module; int frame = 0; std::shared_ptr font; BombeDisplay() { font = Font::load(assetPlugin(plugin, "res/EurostileBold.ttf")); } void draw(NVGcontext *vg) override { nvgFontSize(vg, 14); nvgFontFaceId(vg, font->handle); nvgFillColor(vg, nvgRGBA(255, 0, 0, 0xff)); nvgTextLetterSpacing(vg, -1); char text[128]; for (int i = 0; i < 7; i++) { std::string chordName = ""; std::string chordExtName = ""; if (module->displayBuffer[i].chord != 0) { chordName = CoreUtil().noteNames[module->displayBuffer[i].rootNote] + " " + CoreUtil().ChordTable[module->displayBuffer[i].chord].quality + " " + CoreUtil().inversionNames[module->displayBuffer[i].inversion]; switch(module->mode) { case 0: chordExtName = ""; break; case 1: case 2: case 3: if (module->displayBuffer[i].modeDegree != -1 && module->displayBuffer[i].quality != -1) { // FIXME int index = module->displayBuffer[i].modeDegree * 3 + module->displayBuffer[i].quality; chordExtName = CoreUtil().degreeNames[index]; } break; default: chordExtName = ""; } } snprintf(text, sizeof(text), "%s %s", chordName.c_str(), chordExtName.c_str()); nvgText(vg, box.pos.x + 5, box.pos.y + i * 14, text, NULL); nvgFillColor(vg, nvgRGBA(255 - i * 32, 0, 0, 0xff)); } nvgFillColor(vg, nvgRGBA(255, 0, 0, 0xff)); nvgTextAlign(vg, NVG_ALIGN_RIGHT); snprintf(text, sizeof(text), "%s", module->rootName.c_str()); nvgText(vg, box.size.x - 5, box.pos.y, text, NULL); snprintf(text, sizeof(text), "%s", module->modeName.c_str()); nvgText(vg, box.size.x - 5, box.pos.y + 11, text, NULL); } }; struct BombeWidget : ModuleWidget { Menu *createContextMenu() override; BombeWidget(Bombe *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/Bombe.svg"))); addChild(panel); } { BombeDisplay *display = new BombeDisplay(); display->module = module; display->box.pos = Vec(0, 20); display->box.size = Vec(240, 230); addChild(display); } for (int i = 0; i < 6; i++) { addOutput(Port::create(ui.getPosition(UI::PORT, i, 5, true, false), Port::OUTPUT, module, Bombe::PITCH_OUTPUT + i)); } addParam(ParamWidget::create(ui.getPosition(UI::KNOB, 0, 4, true, false), module, Bombe::KEY_PARAM, 0.0, 11.0, 0.0)); addInput(Port::create(ui.getPosition(UI::PORT, 1, 4, true, false), Port::INPUT, module, Bombe::KEY_INPUT)); addParam(ParamWidget::create(ui.getPosition(UI::KNOB, 2, 4, true, false), module, Bombe::MODE_PARAM, 0.0, 6.0, 0.0)); addInput(Port::create(ui.getPosition(UI::PORT, 3, 4, true, false), Port::INPUT, module, Bombe::MODE_INPUT)); addInput(Port::create(ui.getPosition(UI::PORT, 4, 4, true, false), Port::INPUT, module, Bombe::CLOCK_INPUT)); addParam(ParamWidget::create(ui.getPosition(UI::KNOB, 5, 4, true, false), module, Bombe::LENGTH_PARAM, 2.0, 16.0, 0.0)); Vec XParamPos; XParamPos.x = 33; XParamPos.y = 160; addParam(ParamWidget::create(XParamPos, module, Bombe::X_PARAM, 0.0, 1.0001, 0.5)); Vec XFreezePos; XFreezePos.x = XParamPos.x - 12; XFreezePos.y = XParamPos.y + 60; addInput(Port::create(XFreezePos, Port::INPUT, module, Bombe::FREEZE_INPUT)); Vec XLightPos; XLightPos.x = XParamPos.x + 63; XLightPos.y = XParamPos.y + 68; addChild(ModuleLightWidget::create>(XLightPos, module, Bombe::LOCK_LIGHT)); Vec YParamPos; YParamPos.x = 137; YParamPos.y = 160; addParam(ParamWidget::create(YParamPos, module, Bombe::Y_PARAM, 0.0, 1.0001, 0.5)); Vec YInputPos; YInputPos.x = YParamPos.x - 12; YInputPos.y = YParamPos.y + 60; addInput(Port::create(YInputPos, Port::INPUT, module, Bombe::Y_INPUT)); } }; struct BombeOffsetItem : MenuItem { Bombe *bombe; int offset; void onAction(EventAction &e) override { bombe->offset = offset; } void step() override { rightText = (bombe->offset == offset) ? "✔" : ""; } }; struct BombeModeItem : MenuItem { Bombe *bombe; int mode; void onAction(EventAction &e) override { bombe->mode = mode; } void step() override { rightText = (bombe->mode == mode) ? "✔" : ""; } }; struct BombeInversionsItem : MenuItem { Bombe *bombe; int allowedInversions; void onAction(EventAction &e) override { bombe->allowedInversions = allowedInversions; } void step() override { rightText = (bombe->allowedInversions == allowedInversions) ? "✔" : ""; } }; Menu *BombeWidget::createContextMenu() { Menu *menu = ModuleWidget::createContextMenu(); MenuLabel *spacerLabel = new MenuLabel(); menu->addChild(spacerLabel); Bombe *bombe = dynamic_cast(module); assert(bombe); MenuLabel *offsetLabel = new MenuLabel(); offsetLabel->text = "Repeat Notes"; menu->addChild(offsetLabel); BombeOffsetItem *offsetLowerItem = new BombeOffsetItem(); offsetLowerItem->text = "Lower"; offsetLowerItem->bombe = bombe; offsetLowerItem->offset = 12; menu->addChild(offsetLowerItem); BombeOffsetItem *offsetRepeatItem = new BombeOffsetItem(); offsetRepeatItem->text = "Repeat"; offsetRepeatItem->bombe = bombe; offsetRepeatItem->offset = 24; menu->addChild(offsetRepeatItem); BombeOffsetItem *offsetUpperItem = new BombeOffsetItem(); offsetUpperItem->text = "Upper"; offsetUpperItem->bombe = bombe; offsetUpperItem->offset = 36; menu->addChild(offsetUpperItem); BombeOffsetItem *offsetRandomItem = new BombeOffsetItem(); offsetRandomItem->text = "Random"; offsetRandomItem->bombe = bombe; offsetRandomItem->offset = 0; menu->addChild(offsetRandomItem); MenuLabel *modeLabel = new MenuLabel(); modeLabel->text = "Mode"; menu->addChild(modeLabel); BombeModeItem *modeRandomItem = new BombeModeItem(); modeRandomItem->text = "Random"; modeRandomItem->bombe = bombe; modeRandomItem->mode = 0; menu->addChild(modeRandomItem); BombeModeItem *modeSimpleItem = new BombeModeItem(); modeSimpleItem->text = "Simple"; modeSimpleItem->bombe = bombe; modeSimpleItem->mode = 1; menu->addChild(modeSimpleItem); BombeModeItem *modeGalaxyItem = new BombeModeItem(); modeGalaxyItem->text = "Galaxy"; modeGalaxyItem->bombe = bombe; modeGalaxyItem->mode = 2; menu->addChild(modeGalaxyItem); BombeModeItem *modeComplexItem = new BombeModeItem(); modeComplexItem->text = "Complex"; modeComplexItem->bombe = bombe; modeComplexItem->mode = 3; // menu->addChild(modeComplexItem); MenuLabel *invLabel = new MenuLabel(); invLabel->text = "Allowed Chord Inversions"; menu->addChild(invLabel); BombeInversionsItem *invRootItem = new BombeInversionsItem(); invRootItem->text = "Root only"; invRootItem->bombe = bombe; invRootItem->allowedInversions = 0; menu->addChild(invRootItem); BombeInversionsItem *invFirstItem = new BombeInversionsItem(); invFirstItem->text = "Root and First"; invFirstItem->bombe = bombe; invFirstItem->allowedInversions = 1; menu->addChild(invFirstItem); BombeInversionsItem *invSecondItem = new BombeInversionsItem(); invSecondItem->text = "Root, First and Second"; invSecondItem->bombe = bombe; invSecondItem->allowedInversions = 2; menu->addChild(invSecondItem); return menu; } } // namespace rack_plugin_AmalgamatedHarmonics using namespace rack_plugin_AmalgamatedHarmonics; RACK_PLUGIN_MODEL_INIT(AmalgamatedHarmonics, Bombe) { Model *modelBombe = Model::create( "Amalgamated Harmonics", "Bombe", "Bombe", SEQUENCER_TAG); return modelBombe; } // ♯♭