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.![]()
I appreciate that! Thanks for taking the time to reach out! ![]()
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:
You certainly deserve one!
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
Quick note! I’ll be updating the PatchSeq module soon with additional modules and a search feature for the module browser:
Most importantly, I added ventilation to the front panel to keep things from overheating. As a side-note, I also added a breakbeat module.
Cool. A mixer with more inputs would be great too.
Consider it done.
Ah, ok!
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.
![]()
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. ![]()
Don’t yet tested, but this module have a fantastic look! ![]()
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!!
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.
I liked it
, (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)
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;
}
};
Oh wow, then good job, the interface looks really nice!
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. ![]()

