diff --git a/Makefile b/Makefile
index bebb4cd..01b0036 100644
--- a/Makefile
+++ b/Makefile
@@ -52,6 +52,10 @@ SOURCES += eurorack/rings/resources.cc
SOURCES += eurorack/tides/generator.cc
SOURCES += eurorack/tides/resources.cc
+SOURCES += eurorack/tides2/poly_slope_generator.cc
+SOURCES += eurorack/tides2/ramp_extractor.cc
+SOURCES += eurorack/tides2/resources.cc
+
SOURCES += eurorack/warps/dsp/modulator.cc
SOURCES += eurorack/warps/dsp/oscillator.cc
SOURCES += eurorack/warps/dsp/vocoder.cc
diff --git a/eurorack b/eurorack
index c050f23..dfd1e22 160000
--- a/eurorack
+++ b/eurorack
@@ -1 +1 @@
-Subproject commit c050f232db2f5e45125a48fc634fbf36ca8a08a4
+Subproject commit dfd1e2210c71b28212a2bfe6b50812cd66dab00f
diff --git a/res/Tides2.svg b/res/Tides2.svg
new file mode 100644
index 0000000..cb9d2c3
--- /dev/null
+++ b/res/Tides2.svg
@@ -0,0 +1,1454 @@
+
+
+
+
diff --git a/src/AudibleInstruments.cpp b/src/AudibleInstruments.cpp
index 7657356..eba0a52 100644
--- a/src/AudibleInstruments.cpp
+++ b/src/AudibleInstruments.cpp
@@ -12,6 +12,7 @@ void init(rack::Plugin *p) {
p->addModel(modelPlaits);
p->addModel(modelElements);
p->addModel(modelTides);
+ p->addModel(modelTides2);
p->addModel(modelClouds);
p->addModel(modelWarps);
p->addModel(modelRings);
diff --git a/src/AudibleInstruments.hpp b/src/AudibleInstruments.hpp
index 9539be0..812c1be 100644
--- a/src/AudibleInstruments.hpp
+++ b/src/AudibleInstruments.hpp
@@ -10,6 +10,7 @@ extern Model *modelBraids;
extern Model *modelPlaits;
extern Model *modelElements;
extern Model *modelTides;
+extern Model *modelTides2;
extern Model *modelClouds;
extern Model *modelWarps;
extern Model *modelRings;
diff --git a/src/Tides2.cpp b/src/Tides2.cpp
new file mode 100644
index 0000000..9b973e3
--- /dev/null
+++ b/src/Tides2.cpp
@@ -0,0 +1,299 @@
+#include "AudibleInstruments.hpp"
+#include "dsp/functions.hpp"
+#include "dsp/digital.hpp"
+#include "stmlib/dsp/hysteresis_quantizer.h"
+#include "stmlib/dsp/units.h"
+#include "tides2/poly_slope_generator.h"
+#include "tides2/ramp_extractor.h"
+#include "tides2/io_buffer.h"
+
+
+static const float kRootScaled[3] = {
+ 0.125f,
+ 2.0f,
+ 130.81f
+};
+
+static const tides2::Ratio kRatios[20] = {
+ { 0.0625f, 16 },
+ { 0.125f, 8 },
+ { 0.1666666f, 6 },
+ { 0.25f, 4 },
+ { 0.3333333f, 3 },
+ { 0.5f, 2 },
+ { 0.6666666f, 3 },
+ { 0.75f, 4 },
+ { 0.8f, 5 },
+ { 1, 1 },
+ { 1, 1 },
+ { 1.25f, 4 },
+ { 1.3333333f, 3 },
+ { 1.5f, 2 },
+ { 2.0f, 1 },
+ { 3.0f, 1 },
+ { 4.0f, 1 },
+ { 6.0f, 1 },
+ { 8.0f, 1 },
+ { 16.0f, 1 },
+};
+
+
+struct Tides2 : Module {
+ enum ParamIds {
+ RANGE_PARAM,
+ MODE_PARAM,
+ RAMP_PARAM,
+ FREQUENCY_PARAM,
+ SHAPE_PARAM,
+ SMOOTHNESS_PARAM,
+ SLOPE_PARAM,
+ SHIFT_PARAM,
+ SLOPE_CV_PARAM,
+ FREQUENCY_CV_PARAM,
+ SMOOTHNESS_CV_PARAM,
+ SHAPE_CV_PARAM,
+ SHIFT_CV_PARAM,
+ NUM_PARAMS
+ };
+ enum InputIds {
+ SLOPE_INPUT,
+ FREQUENCY_INPUT,
+ V_OCT_INPUT,
+ SMOOTHNESS_INPUT,
+ SHAPE_INPUT,
+ SHIFT_INPUT,
+ TRIG_INPUT,
+ CLOCK_INPUT,
+ NUM_INPUTS
+ };
+ enum OutputIds {
+ ENUMS(OUT_OUTPUTS, 4),
+ NUM_OUTPUTS
+ };
+ enum LightIds {
+ ENUMS(RANGE_LIGHT, 2),
+ ENUMS(OUTPUT_MODE_LIGHT, 2),
+ ENUMS(RAMP_MODE_LIGHT, 2),
+ ENUMS(OUTPUT_LIGHTS, 4),
+ NUM_LIGHTS
+ };
+
+ tides2::PolySlopeGenerator poly_slope_generator;
+ tides2::RampExtractor ramp_extractor;
+ stmlib::HysteresisQuantizer ratio_index_quantizer;
+
+ // State
+ int range;
+ tides2::OutputMode output_mode;
+ tides2::RampMode ramp_mode;
+ BooleanTrigger rangeTrigger;
+ BooleanTrigger modeTrigger;
+ BooleanTrigger rampTrigger;
+
+ // Buffers
+ tides2::PolySlopeGenerator::OutputSample out[tides2::kBlockSize];
+ stmlib::GateFlags trig_flags[tides2::kBlockSize];
+ stmlib::GateFlags clock_flags[tides2::kBlockSize];
+ stmlib::GateFlags previous_trig_flag = stmlib::GATE_FLAG_LOW;
+ stmlib::GateFlags previous_clock_flag = stmlib::GATE_FLAG_LOW;
+
+ bool must_reset_ramp_extractor = true;
+ tides2::OutputMode previous_output_mode = tides2::OUTPUT_MODE_GATES;
+ uint8_t frame = 0;
+
+ Tides2() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) {
+ poly_slope_generator.Init();
+ ratio_index_quantizer.Init();
+ memset(&out, 0, sizeof(out));
+ memset(&trig_flags, 0, sizeof(trig_flags));
+ memset(&clock_flags, 0, sizeof(clock_flags));
+ onReset();
+ onSampleRateChange();
+ }
+
+ void onReset() override {
+ range = 1;
+ output_mode = tides2::OUTPUT_MODE_GATES;
+ ramp_mode = tides2::RAMP_MODE_LOOPING;
+ }
+
+ void onRandomize() override {
+ range = randomu32() % 3;
+ output_mode = (tides2::OutputMode) (randomu32() % 4);
+ ramp_mode = (tides2::RampMode) (randomu32() % 3);
+ }
+
+ void onSampleRateChange() override {
+ ramp_extractor.Init(engineGetSampleRate(), 40.f / engineGetSampleRate());
+ }
+
+ json_t *toJson() override {
+ json_t *rootJ = json_object();
+
+ json_object_set_new(rootJ, "range", json_integer(range));
+ json_object_set_new(rootJ, "output", json_integer(output_mode));
+ json_object_set_new(rootJ, "ramp", json_integer(ramp_mode));
+
+ return rootJ;
+ }
+
+ void fromJson(json_t *rootJ) override {
+ json_t *rangeJ = json_object_get(rootJ, "range");
+ if (rangeJ)
+ range = json_integer_value(rangeJ);
+
+ json_t *outputJ = json_object_get(rootJ, "output");
+ if (outputJ)
+ output_mode = (tides2::OutputMode) json_integer_value(outputJ);
+
+ json_t *rampJ = json_object_get(rootJ, "ramp");
+ if (rampJ)
+ ramp_mode = (tides2::RampMode) json_integer_value(rampJ);
+ }
+
+ void step() override {
+ // Switches
+ if (rangeTrigger.process(params[RANGE_PARAM].value > 0.f)) {
+ range = (range + 1) % 3;
+ }
+ if (modeTrigger.process(params[MODE_PARAM].value > 0.f)) {
+ output_mode = (tides2::OutputMode) ((output_mode + 1) % 4);
+ }
+ if (rampTrigger.process(params[RAMP_PARAM].value > 0.f)) {
+ ramp_mode = (tides2::RampMode) ((ramp_mode + 1) % 3);
+ }
+
+ // Input gates
+ trig_flags[frame] = stmlib::ExtractGateFlags(previous_trig_flag, inputs[TRIG_INPUT].value >= 1.7f);
+ previous_trig_flag = trig_flags[frame];
+
+ clock_flags[frame] = stmlib::ExtractGateFlags(previous_clock_flag, inputs[CLOCK_INPUT].value >= 1.7f);
+ previous_clock_flag = clock_flags[frame];
+
+ // Process block
+ if (++frame >= tides2::kBlockSize) {
+ frame = 0;
+
+ tides2::Range range_mode = (range < 2) ? tides2::RANGE_CONTROL : tides2::RANGE_AUDIO;
+ float note = clamp(params[FREQUENCY_PARAM].value + 12.f * inputs[V_OCT_INPUT].value, -96.f, 96.f);
+ float fm = clamp(params[FREQUENCY_CV_PARAM].value * inputs[FREQUENCY_INPUT].value * 12.f, -96.f, 96.f);
+ float transposition = note + fm;
+
+ float ramp[tides2::kBlockSize];
+ float frequency;
+
+ if (inputs[CLOCK_INPUT].active) {
+ if (must_reset_ramp_extractor) {
+ ramp_extractor.Reset();
+ }
+
+ tides2::Ratio r = ratio_index_quantizer.Lookup(kRatios, 0.5f + transposition * 0.0105f, 20);
+ frequency = ramp_extractor.Process(
+ range_mode == tides2::RANGE_AUDIO,
+ range_mode == tides2::RANGE_AUDIO && ramp_mode == tides2::RAMP_MODE_AR,
+ r,
+ clock_flags,
+ ramp,
+ tides2::kBlockSize);
+ must_reset_ramp_extractor = false;
+ }
+ else {
+ frequency = kRootScaled[range] / engineGetSampleRate() * stmlib::SemitonesToRatio(transposition);
+ must_reset_ramp_extractor = true;
+ }
+
+ // Get parameters
+ float slope = clamp(params[SLOPE_PARAM].value + cubic(params[SLOPE_CV_PARAM].value) * inputs[SLOPE_INPUT].value / 10.f, 0.f, 1.f);
+ float shape = clamp(params[SHAPE_PARAM].value + cubic(params[SHAPE_CV_PARAM].value) * inputs[SHAPE_INPUT].value / 10.f, 0.f, 1.f);
+ float smoothness = clamp(params[SMOOTHNESS_PARAM].value + cubic(params[SMOOTHNESS_CV_PARAM].value) * inputs[SMOOTHNESS_INPUT].value / 10.f, 0.f, 1.f);
+ float shift = clamp(params[SHIFT_PARAM].value + cubic(params[SHIFT_CV_PARAM].value) * inputs[SHIFT_INPUT].value / 10.f, 0.f, 1.f);
+
+ if (output_mode != previous_output_mode) {
+ poly_slope_generator.Reset();
+ previous_output_mode = output_mode;
+ }
+
+ // Render generator
+ poly_slope_generator.Render(
+ ramp_mode,
+ output_mode,
+ range_mode,
+ frequency,
+ slope,
+ shape,
+ smoothness,
+ shift,
+ trig_flags,
+ !inputs[TRIG_INPUT].active && inputs[CLOCK_INPUT].active ? ramp : NULL,
+ out,
+ tides2::kBlockSize);
+
+ // Set lights
+ lights[RANGE_LIGHT + 0].value = (range == 0 || range == 1);
+ lights[RANGE_LIGHT + 1].value = (range == 1 || range == 2);
+ lights[OUTPUT_MODE_LIGHT + 0].value = (output_mode == tides2::OUTPUT_MODE_AMPLITUDE || output_mode == tides2::OUTPUT_MODE_SLOPE_PHASE);
+ lights[OUTPUT_MODE_LIGHT + 1].value = (output_mode == tides2::OUTPUT_MODE_FREQUENCY || output_mode == tides2::OUTPUT_MODE_SLOPE_PHASE);
+ lights[RAMP_MODE_LIGHT + 0].value = (ramp_mode == tides2::RAMP_MODE_AD || ramp_mode == tides2::RAMP_MODE_LOOPING);
+ lights[RAMP_MODE_LIGHT + 1].value = (ramp_mode == tides2::RAMP_MODE_AR || ramp_mode == tides2::RAMP_MODE_LOOPING);
+ }
+
+ // Outputs
+ for (int i = 0; i < 4; i++) {
+ float value = out[frame].channel[i];
+ outputs[OUT_OUTPUTS + i].value = value;
+ lights[OUTPUT_LIGHTS + i].setBrightnessSmooth(value);
+ }
+ }
+};
+
+
+struct Tides2Widget : ModuleWidget {
+ Tides2Widget(Tides2 *module) : ModuleWidget(module) {
+ setPanel(SVG::load(assetPlugin(plugin, "res/Tides2.svg")));
+
+ addChild(createWidget(Vec(RACK_GRID_WIDTH, 0)));
+ addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0)));
+ addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
+ addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
+
+ addParam(createParamCentered(mm2px(Vec(7.425, 16.15)), module, Tides2::RANGE_PARAM, 0.0, 1.0, 0.0));
+ addParam(createParamCentered(mm2px(Vec(63.325, 16.15)), module, Tides2::MODE_PARAM, 0.0, 1.0, 0.0));
+ addParam(createParamCentered(mm2px(Vec(16.325, 33.449)), module, Tides2::FREQUENCY_PARAM, -48, 48, 0.0));
+ addParam(createParamCentered(mm2px(Vec(54.425, 33.449)), module, Tides2::SHAPE_PARAM, 0.0, 1.0, 0.5));
+ addParam(createParamCentered(mm2px(Vec(35.375, 38.699)), module, Tides2::RAMP_PARAM, 0.0, 1.0, 0.0));
+ addParam(createParamCentered(mm2px(Vec(35.375, 55.549)), module, Tides2::SMOOTHNESS_PARAM, 0.0, 1.0, 0.5));
+ addParam(createParamCentered(mm2px(Vec(11.575, 60.599)), module, Tides2::SLOPE_PARAM, 0.0, 1.0, 0.5));
+ addParam(createParamCentered(mm2px(Vec(59.175, 60.599)), module, Tides2::SHIFT_PARAM, 0.0, 1.0, 0.5));
+ addParam(createParamCentered(mm2px(Vec(9.276, 80.599)), module, Tides2::SLOPE_CV_PARAM, -1.0, 1.0, 0.0));
+ addParam(createParamCentered(mm2px(Vec(22.324, 80.599)), module, Tides2::FREQUENCY_CV_PARAM, -1.0, 1.0, 0.0));
+ addParam(createParamCentered(mm2px(Vec(35.375, 80.599)), module, Tides2::SMOOTHNESS_CV_PARAM, -1.0, 1.0, 0.0));
+ addParam(createParamCentered(mm2px(Vec(48.425, 80.599)), module, Tides2::SHAPE_CV_PARAM, -1.0, 1.0, 0.0));
+ addParam(createParamCentered(mm2px(Vec(61.475, 80.599)), module, Tides2::SHIFT_CV_PARAM, -1.0, 1.0, 0.0));
+
+ addInput(createInputCentered(mm2px(Vec(6.775, 96.499)), module, Tides2::SLOPE_INPUT));
+ addInput(createInputCentered(mm2px(Vec(18.225, 96.499)), module, Tides2::FREQUENCY_INPUT));
+ addInput(createInputCentered(mm2px(Vec(29.675, 96.499)), module, Tides2::V_OCT_INPUT));
+ addInput(createInputCentered(mm2px(Vec(41.125, 96.499)), module, Tides2::SMOOTHNESS_INPUT));
+ addInput(createInputCentered(mm2px(Vec(52.575, 96.499)), module, Tides2::SHAPE_INPUT));
+ addInput(createInputCentered(mm2px(Vec(64.025, 96.499)), module, Tides2::SHIFT_INPUT));
+ addInput(createInputCentered(mm2px(Vec(6.775, 111.099)), module, Tides2::TRIG_INPUT));
+ addInput(createInputCentered(mm2px(Vec(18.225, 111.099)), module, Tides2::CLOCK_INPUT));
+
+ addOutput(createOutputCentered(mm2px(Vec(29.675, 111.099)), module, Tides2::OUT_OUTPUTS + 0));
+ addOutput(createOutputCentered(mm2px(Vec(41.125, 111.099)), module, Tides2::OUT_OUTPUTS + 1));
+ addOutput(createOutputCentered(mm2px(Vec(52.575, 111.099)), module, Tides2::OUT_OUTPUTS + 2));
+ addOutput(createOutputCentered(mm2px(Vec(64.025, 111.099)), module, Tides2::OUT_OUTPUTS + 3));
+
+ addChild(createLightCentered>(mm2px(Vec(13.776, 16.149)), module, Tides2::RANGE_LIGHT));
+ addChild(createLightCentered>(mm2px(Vec(56.975, 16.149)), module, Tides2::OUTPUT_MODE_LIGHT));
+ addChild(createLightCentered>(mm2px(Vec(35.375, 33.449)), module, Tides2::RAMP_MODE_LIGHT));
+ addChild(createLightCentered>(mm2px(Vec(26.174, 104.749)), module, Tides2::OUTPUT_LIGHTS + 0));
+ addChild(createLightCentered>(mm2px(Vec(37.625, 104.749)), module, Tides2::OUTPUT_LIGHTS + 1));
+ addChild(createLightCentered>(mm2px(Vec(49.075, 104.749)), module, Tides2::OUTPUT_LIGHTS + 2));
+ addChild(createLightCentered>(mm2px(Vec(60.525, 104.749)), module, Tides2::OUTPUT_LIGHTS + 3));
+ }
+};
+
+
+Model *modelTides2 = createModel("Audible Instruments", "Tides2", "Tidal Modulator 2");
+