Voxglitch Community Feedback

Hey Bret…That’s weird. I just saw that Voxglitch PatchSeq is listed on VCV for $10… I haven’t bought Voxglitch PatchSeq, but I have it in my VCV library. You giving it to me as a gift?.. ..Somewhere there’s a glitch.:pleading_face:

I appreciate that! Thanks for taking the time to reach out! :clinking_beer_mugs:

Hmm, I didn’t gift it to you. It’s possible that it is being included in the VCV Rack+ subscription. Do you have that? I did talk to Andrew about being part of the VCV Rack Plus (VCV+) subscription, but I don’t recall if it was just a one-off, or my entire collection. I’ll verify this with Andrew and report back. If so, yay!!

In the meantime, let me present this emoji of a flower as a gift: :blossom: You certainly deserve one!

4 Likes

Just inform - i have no such glitch with false license for PatchSeq (despite i had all other payable VG stuff). Ie not see it in my collection. Think to grab soon, looks fun

1 Like

Quick note! I’ll be updating the PatchSeq module soon with additional modules and a search feature for the module browser:

1 Like

Most importantly, I added ventilation to the front panel to keep things from overheating. As a side-note, I also added a breakbeat module.

7 Likes

Cool. A mixer with more inputs would be great too.

Consider it done.

2 Likes

Ah, ok!

:megaphone: Listen up everyone!

I got clarification from Andrew that all Voxglitch Devices modules (the paid ones) are free for all VCV Rack Plus users. That’s a pretty sweet deal. If Voxglitch Devices are magically appearing in your collection, I would wager that you’re subscribed to VCV Rack Plus.

I get a small percentage of VCV Rack Plus proceeds.

It seems you’ve lost that wager, as I never signed up for VCV Rack Plus. I believe that these glitches are the fault of the VCV company! As consolation, I’ll give you a tomato and two chilies. :hot_pepper: :tomato: :hot_pepper:

1 Like

Now I’m feeling a little embarrassed by my single flower offering. Thanks for reporting back. I’ll pass this info along to Andrew and maybe he can figure it out. :thinking:

Don’t yet tested, but this module have a fantastic look! :heart:

Howdy! I’m just making sure I haven’t missed out on an update, or some other goofy gremlins…

I don’t see the any of the aforementioned updates ( Voxglitch Community Feedback - #1593 by clone45 ) in my version of Wav Bank MC…

FWIW, I tried deleting the plugin directory & ‘re-updating’, but…. no soap!!

1 Like

I’m doing some work on the collection today, so I’ll double check on it. There’s definitely a chance that I just didn’t get an updated submitted yet.

1 Like

I liked it :+1:, (PatchSeq) btw found that it even fun to use just as sound\drone generator. Up to create some complex raw textures lol. (mini vcv inside vcv)

2 Likes

May I ask which UI framework you are using for PatchSeq?

Hi Ben! It’s not really a framework. I might be able to turn it into a framework, but it would take a bit of time.

Here’s an example of some of the code:

#pragma once

#include "GAParameterWidget.hpp"
#include <cmath>
#include <algorithm>

// ============================================================================
// GAKnobWidget - Circular knob for continuous or discrete values
// ============================================================================

class GAKnobWidget : public GAParameterWidget {
public:
    // Value range
    float minValue = 0.0f;
    float maxValue = 1.0f;
    float defaultValue = 0.5f;

    // Pointer to the actual value in GAModule
    float* valuePtr = nullptr;

    // Behavior flags
    bool isInteger = false;      // Snap to integer values
    bool isExponential = false;  // Logarithmic scaling for frequency/rate knobs

    // Drag state
    float dragStartValue = 0.0f;
    float dragAccumulator = 0.0f;

    // Optional: discrete value labels (for waveform selectors, etc.)
    std::vector<std::string> discreteLabels;

    GAKnobWidget() {
        width = 32.0f;
        height = 32.0f;
    }

    GAKnobWidget(std::string _name, float _min, float _max, float _default, float* _ptr,
                 bool _isInt = false, bool _isExp = false)
        : minValue(_min), maxValue(_max), defaultValue(_default),
          valuePtr(_ptr), isInteger(_isInt), isExponential(_isExp) {
        name = _name;
        width = 32.0f;
        height = 32.0f;
    }

    // ========================================================================
    // Value conversion (linear vs exponential)
    // ========================================================================

    // Convert actual value to normalized 0-1 position for display
    float valueToNormalized(float value) {
        if (maxValue <= minValue) return 0.0f;
        if (isExponential && minValue > 0.0f) {
            // Logarithmic: position = log(value/min) / log(max/min)
            float logMin = std::log(minValue);
            float logMax = std::log(maxValue);
            float logVal = std::log(std::max(minValue, std::min(maxValue, value)));
            return (logVal - logMin) / (logMax - logMin);
        } else {
            // Linear
            return (value - minValue) / (maxValue - minValue);
        }
    }

    // Convert normalized 0-1 position to actual value
    float normalizedToValue(float normalized) {
        normalized = std::max(0.0f, std::min(1.0f, normalized));
        if (isExponential && minValue > 0.0f) {
            // Exponential: value = min * (max/min)^position
            return minValue * std::pow(maxValue / minValue, normalized);
        } else {
            // Linear
            return minValue + normalized * (maxValue - minValue);
        }
    }

    // ========================================================================
    // GAParameterWidget interface
    // ========================================================================

    void draw(NVGcontext* vg, float dim) override {
        float cx = position.x;
        float cy = position.y;
        float radius = width / 2 - 2;

        // Get normalized value
        float value = 0.5f;
        if (valuePtr) {
            value = valueToNormalized(*valuePtr);
        }

        // Knob background
        nvgBeginPath(vg);
        nvgCircle(vg, cx, cy, radius);
        NVGcolor bgColor = isHovered ? nvgRGB(55, 65, 80) : nvgRGB(45, 52, 62);
        if (isDragging) bgColor = nvgRGB(65, 75, 90);
        nvgFillColor(vg, bgColor);
        nvgFill(vg);

        // Knob border
        nvgBeginPath(vg);
        nvgCircle(vg, cx, cy, radius);
        nvgStrokeColor(vg, nvgRGBA(80, 95, 110, (int)(255 * dim)));
        nvgStrokeWidth(vg, 1.5f);
        nvgStroke(vg);

        // Arc showing value (from bottom-left to position)
        float startAngle = 0.75f * M_PI;  // 135 degrees (bottom-left)
        float endAngle = startAngle + value * 1.5f * M_PI;  // 270 degree sweep

        nvgBeginPath(vg);
        nvgArc(vg, cx, cy, radius - 3, startAngle, endAngle, NVG_CW);
        nvgStrokeColor(vg, nvgRGBA(100, 140, 180, (int)(255 * dim)));
        nvgStrokeWidth(vg, 3.0f);
        nvgLineCap(vg, NVG_ROUND);
        nvgStroke(vg);

        // Pointer line
        float pointerAngle = startAngle + value * 1.5f * M_PI;
        float innerR = 4.0f;
        float outerR = radius - 5;
        nvgBeginPath(vg);
        nvgMoveTo(vg, cx + std::cos(pointerAngle) * innerR,
                      cy + std::sin(pointerAngle) * innerR);
        nvgLineTo(vg, cx + std::cos(pointerAngle) * outerR,
                      cy + std::sin(pointerAngle) * outerR);
        nvgStrokeColor(vg, nvgRGBA(200, 210, 220, (int)(255 * dim)));
        nvgStrokeWidth(vg, 2.0f);
        nvgStroke(vg);
    }

    void onDragStart(Vec pos) override {
        GAParameterWidget::onDragStart(pos);
        dragAccumulator = 0.0f;
        if (valuePtr) {
            dragStartValue = *valuePtr;
        }
    }

    void onDragMove(Vec delta, float accumulator) override {
        if (!valuePtr) return;

        float previousValue = *valuePtr;
        dragAccumulator = accumulator;

        float newValue;
        if (isInteger) {
            // For integers: calculate from start value + accumulated pixels
            // Change one step per ~20 pixels of drag
            float steps = dragAccumulator / 20.0f;
            newValue = dragStartValue + steps;
            newValue = std::round(newValue);
        } else if (isExponential) {
            // Exponential knobs: drag in normalized space for uniform feel
            // Full range over ~100 pixels of drag
            float startNormalized = valueToNormalized(dragStartValue);
            float normalizedDelta = dragAccumulator / 100.0f;
            float newNormalized = startNormalized + normalizedDelta;
            newValue = normalizedToValue(newNormalized);
        } else {
            // Linear continuous knobs: full range over ~100 pixels
            float range = maxValue - minValue;
            float sensitivity = range / 100.0f;
            newValue = dragStartValue + dragAccumulator * sensitivity;
        }

        // Clamp to range
        newValue = std::max(minValue, std::min(maxValue, newValue));
        *valuePtr = newValue;

        // Notify only when value actually changes
        if (isInteger) {
            if ((int)newValue != (int)previousValue) {
                notifyValueChanged();
            }
        } else {
            if (std::abs(newValue - previousValue) > 0.0001f) {
                notifyValueChanged();
            }
        }
    }

    void onDragEnd() override {
        GAParameterWidget::onDragEnd();
        dragAccumulator = 0.0f;
    }

    void onDoubleClick() override {
        if (valuePtr) {
            *valuePtr = defaultValue;
            notifyValueChanged();
        }
    }

    float getValue() override {
        return valuePtr ? *valuePtr : 0.0f;
    }

    void setValue(float v) override {
        if (valuePtr) {
            *valuePtr = std::max(minValue, std::min(maxValue, v));
        }
    }

    void resetToDefault() override {
        if (valuePtr) {
            *valuePtr = defaultValue;
            notifyValueChanged();
        }
    }

    std::string getLabel() override {
        if (isInteger && !discreteLabels.empty() && valuePtr) {
            int idx = (int)*valuePtr;
            if (idx >= 0 && idx < (int)discreteLabels.size()) {
                return name + "(" + discreteLabels[idx] + ")";
            }
        }
        return name;
    }

    std::string getValueString() override {
        if (!valuePtr) return "";

        float value = *valuePtr;
        char buf[32];

        if (isInteger) {
            snprintf(buf, sizeof(buf), "%s: %d", name.c_str(), (int)value);
        } else if (std::abs(value) < 0.01f && value != 0.0f) {
            snprintf(buf, sizeof(buf), "%s: %.4f", name.c_str(), value);
        } else if (std::abs(value) < 1.0f) {
            snprintf(buf, sizeof(buf), "%s: %.3f", name.c_str(), value);
        } else if (std::abs(value) < 100.0f) {
            snprintf(buf, sizeof(buf), "%s: %.2f", name.c_str(), value);
        } else {
            snprintf(buf, sizeof(buf), "%s: %.1f", name.c_str(), value);
        }

        return std::string(buf);
    }

    // Set discrete labels for integer knobs (waveform names, mode names, etc.)
    void setDiscreteLabels(std::vector<std::string> labels) {
        discreteLabels = labels;
    }
};

1 Like

Oh wow, then good job, the interface looks really nice!

1 Like

Thanks! I might be making some updates for the upcoming Groovebox Advanced so that the entire canvas can be used for parameters, not just the right-hand column. I’m building in a mini-303 engine and separate sequencer into the Groovebox Advanced, and I need that extra room for the classic TB-303 style interface. It’ll make more sense when I can show it in action. :camera:

4 Likes