| @@ -2,8 +2,6 @@ | |||
| #include <samplerate.h> | |||
| #define HISTORY_SIZE (1<<21) | |||
| struct Delay : Module { | |||
| enum ParamId { | |||
| TIME_PARAM, | |||
| @@ -34,20 +32,33 @@ struct Delay : Module { | |||
| NUM_OUTPUTS | |||
| }; | |||
| enum LightId { | |||
| PERIOD_LIGHT, | |||
| CLOCK_LIGHT, | |||
| NUM_LIGHTS | |||
| }; | |||
| constexpr static size_t HISTORY_SIZE = 1 << 21; | |||
| dsp::DoubleRingBuffer<float, HISTORY_SIZE> historyBuffer; | |||
| dsp::DoubleRingBuffer<float, 16> outBuffer; | |||
| SRC_STATE* src; | |||
| float lastWet = 0.f; | |||
| dsp::RCFilter lowpassFilter; | |||
| dsp::RCFilter highpassFilter; | |||
| float clockFreq = 1.f; | |||
| dsp::Timer clockTimer; | |||
| dsp::SchmittTrigger clockTrigger; | |||
| float clockPhase = 0.f; | |||
| Delay() { | |||
| config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); | |||
| configParam(TIME_PARAM, 0.f, 1.f, 0.5f, "Time", " s", 10.f / 1e-3, 1e-3); | |||
| // This was made before the pitch voltage standard existed, so it uses TIME_PARAM = 0 as 0.001s and TIME_PARAM = 1 as 10s with a formula of: | |||
| // time = 0.001 * 10000^TIME_PARAM | |||
| // or | |||
| // TIME_PARAM = log10(time * 1000) / 4 | |||
| const float timeMin = log10(0.001f * 1000) / 4; | |||
| const float timeMax = log10(10.f * 1000) / 4; | |||
| const float timeDefault = log10(0.5f * 1000) / 4; | |||
| configParam(TIME_PARAM, timeMin, timeMax, timeDefault, "Time", " s", 10.f / 1e-3, 1e-3); | |||
| configParam(FEEDBACK_PARAM, 0.f, 1.f, 0.5f, "Feedback", "%", 0, 100); | |||
| configParam(TONE_PARAM, 0.f, 1.f, 0.5f, "Tone", "%", 0, 200, -100); | |||
| configParam(MIX_PARAM, 0.f, 1.f, 0.5f, "Mix", "%", 0, 100); | |||
| @@ -61,6 +72,7 @@ struct Delay : Module { | |||
| getParamQuantity(MIX_CV_PARAM)->randomizeEnabled = false; | |||
| configInput(TIME_INPUT, "Time"); | |||
| getInputInfo(TIME_INPUT)->description = "1V/octave when Time CV is 100%"; | |||
| configInput(FEEDBACK_INPUT, "Feedback"); | |||
| configInput(TONE_INPUT, "Tone"); | |||
| configInput(MIX_INPUT, "Mix"); | |||
| @@ -79,34 +91,54 @@ struct Delay : Module { | |||
| } | |||
| void process(const ProcessArgs& args) override { | |||
| // Clock | |||
| if (inputs[CLOCK_INPUT].isConnected()) { | |||
| clockTimer.process(args.sampleTime); | |||
| if (clockTrigger.process(inputs[CLOCK_INPUT].getVoltage(), 0.1f, 2.f)) { | |||
| float clockFreq = 1.f / clockTimer.getTime(); | |||
| clockTimer.reset(); | |||
| if (0.001f <= clockFreq && clockFreq <= 1000.f) { | |||
| this->clockFreq = clockFreq; | |||
| } | |||
| } | |||
| } | |||
| else { | |||
| // Default frequency when clock is unpatched | |||
| clockFreq = 2.f; | |||
| } | |||
| // Get input to delay block | |||
| float in = inputs[IN_INPUT].getVoltageSum(); | |||
| float feedback = params[FEEDBACK_PARAM].getValue() + inputs[FEEDBACK_INPUT].getVoltage() / 10.f * params[FEEDBACK_CV_PARAM].getValue(); | |||
| feedback = clamp(feedback, 0.f, 1.f); | |||
| float dry = in + lastWet * feedback; | |||
| // Compute delay time in seconds | |||
| float delay = params[TIME_PARAM].getValue() + inputs[TIME_INPUT].getVoltage() / 10.f * params[TIME_CV_PARAM].getValue(); | |||
| delay = clamp(delay, 0.f, 1.f); | |||
| delay = 1e-3 * std::pow(10.f / 1e-3, delay); | |||
| // Number of delay samples | |||
| float index = std::round(delay * args.sampleRate); | |||
| // Compute freq | |||
| // Scale time knob to 1V/oct pitch based on formula explained in constructor, for backwards compatibility | |||
| float pitch = std::log2(1000.f) - std::log2(10000.f) * params[TIME_PARAM].getValue(); | |||
| pitch += inputs[TIME_INPUT].getVoltage() * params[TIME_CV_PARAM].getValue(); | |||
| float freq = clockFreq / 2.f * std::pow(2.f, pitch); | |||
| // Number of desired delay samples | |||
| float index = args.sampleRate / freq; | |||
| // In order to delay accurate samples, subtract by the historyBuffer size, and an experimentally tweaked amount. | |||
| index -= 16 + 4.f; | |||
| index = clamp(index, 2.f, float(HISTORY_SIZE - 1)); | |||
| // DEBUG("freq %f index %f", freq, index); | |||
| // Push dry sample into history buffer | |||
| if (!historyBuffer.full()) { | |||
| historyBuffer.push(dry); | |||
| } | |||
| // How many samples do we need consume to catch up? | |||
| float consume = index - historyBuffer.size(); | |||
| if (outBuffer.empty()) { | |||
| double ratio = 1.f; | |||
| if (std::fabs(consume) >= 16.f) { | |||
| // Here's where the delay magic is. Smooth the ratio depending on how divergent we are from the correct delay time. | |||
| ratio = std::pow(10.f, clamp(consume / 10000.f, -1.f, 1.f)); | |||
| } | |||
| if (outBuffer.empty() && historyBuffer.size() >= 2) { | |||
| // How many samples do we need consume to catch up? | |||
| float consume = index - historyBuffer.size(); | |||
| double ratio = std::pow(2.f, clamp(consume / 1000.f, -1.f, 1.f)); | |||
| // DEBUG("index %f historyBuffer %lu consume %f ratio %f", index, historyBuffer.size(), consume, ratio); | |||
| // Convert samples from the historyBuffer to catch up or slow down so `index` and `historyBuffer.size()` eventually match approximately | |||
| SRC_DATA srcData; | |||
| srcData.data_in = (const float*) historyBuffer.startData(); | |||
| srcData.data_out = (float*) outBuffer.endData(); | |||
| @@ -139,20 +171,34 @@ struct Delay : Module { | |||
| highpassFilter.process(wet); | |||
| wet = highpassFilter.highpass(); | |||
| // Set wet output | |||
| outputs[WET_OUTPUT].setVoltage(wet); | |||
| lastWet = wet; | |||
| // Set mix output | |||
| float mix = params[MIX_PARAM].getValue() + inputs[MIX_INPUT].getVoltage() / 10.f * params[MIX_CV_PARAM].getValue(); | |||
| mix = clamp(mix, 0.f, 1.f); | |||
| float out = crossfade(in, wet, mix); | |||
| outputs[MIX_OUTPUT].setVoltage(out); | |||
| // Clock light | |||
| clockPhase += freq * args.sampleTime; | |||
| if (clockPhase >= 1.f) { | |||
| clockPhase -= 1.f; | |||
| lights[CLOCK_LIGHT].setBrightness(1.f); | |||
| } | |||
| else { | |||
| lights[CLOCK_LIGHT].setBrightnessSmooth(0.f, args.sampleTime); | |||
| } | |||
| } | |||
| void fromJson(json_t* rootJ) override { | |||
| // These attenuators didn't exist in version <2.0, so set to 1 for default compatibility. | |||
| params[TIME_CV_PARAM].setValue(1.f); | |||
| // These attenuators didn't exist in version <2.0, so set to 1 in case they are not overwritten. | |||
| params[FEEDBACK_CV_PARAM].setValue(1.f); | |||
| params[TONE_CV_PARAM].setValue(1.f); | |||
| params[MIX_CV_PARAM].setValue(1.f); | |||
| // The time input scaling has changed, so don't set to 1. | |||
| // params[TIME_CV_PARAM].setValue(1.f); | |||
| Module::fromJson(rootJ); | |||
| } | |||
| @@ -188,7 +234,7 @@ struct DelayWidget : ModuleWidget { | |||
| addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(28.278, 113.115)), module, Delay::WET_OUTPUT)); | |||
| addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(39.115, 113.115)), module, Delay::MIX_OUTPUT)); | |||
| addChild(createLightCentered<SmallLight<YellowLight>>(mm2px(Vec(22.738, 16.428)), module, Delay::PERIOD_LIGHT)); | |||
| addChild(createLightCentered<SmallLight<YellowLight>>(mm2px(Vec(22.738, 16.428)), module, Delay::CLOCK_LIGHT)); | |||
| } | |||
| }; | |||