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 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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"); +