Saturday, April 25, 2026

Gakken LFO Modifications (and Faking Log Pots with Code)

    We're continuing to look at the Gakken SX-150 mk II and investigate how we can improve it. Today we're focusing on the LFO. It's a classic design: integrator and schmitt trigger. It even has a crude reset feature.

Gakken SX-150 mk II stock LFO

MIDI

    As with everything else, we'd like to add MIDI control. We can substitute a digipot in place of the rate potentiometer and call it as day, right? Well, yes, but the original pot isn't doing it's job particularly well. The lower end of the frequency range has a jump in it that makes it quite touchy, as does the top, and most of the rest of the range yields overly-fast results.

It's Log?

    What went wrong? The designers avoided using log pots, and this is a situation that calls for a log pot. They instead approximated a log taper by putting R66 in parallel with half of the linear pot. Out of curiosity, we can break the circuit down and figure out what the actual taper is.

    We can think of the pot as two resistances that are joined by the wiper, forming a voltage divider. The ratio of resistances determines what proportion of the input voltage we'll get at the output. If we let `t` represent the top resistance, and `b` represent the bottom, this formula gives the ratio between input and output:

`b/(t+b)`

    Since these two resistances are just portions of the same pot, they'll always sum to the full resistance of the pot. If we use `x` to represent the position of the pot from 0-1, and `r_1` for the total resistance, we can define `t` and `b` like so:

`t=r_1(1-x)`
`b=r_1x`

    Now we just need to include the resistor that's in parallel with `b`. The parallel resistor formula is:

`(r_1r_2)/(r_1+r_2)`

    We can plug in `b` and our fixed resistor of 2.2(k) to get the actual value for the bottom half of the divider, that we'll call `B`:

    `B=(2.2b)/(2.2+b)`

    This takes the place of `b` in our first formula, and `r_1 = 50`, giving us this stack of formulas that we can put into Desmos:

`t=50(1-x)`

`b=50x`

`B=(2.2b)/(2.2+b)`

`y=B/(t+B)`

Gakken fake log taper

    We can see there's a funny lip at the low extreme that explains it being touchy. The high side shoots up rather steeply also. While it's better than a linear response, it's not a good enough log approximation for this LFO. If we naively put a digipot in its place, we'll have the exact same issues.

Better than Bad

    Since MIDI CCs only have 128 steps, and our digipots have 256, there's a little wiggle room in how we progress through the positions. We can map the 128 steps onto positions that help compensate for the inaccuracies of the log approximation.

    We'll define a curve to do this compensation, but it has to be one that can be realized with the limited positions of the digipot. It we pick a curve that's too shallow, one step of the CC change won't push us up to the next position of the digipot, and we'll have wasted one of our precious 128 steps.

digipot trying to resolve shallow curves

    Above we can see a line in green that yields 2 digipot steps per 1 CC step, and an orange line that yields 1 digipot step per 1 CC step. Finally we have the red line that requires less than one digipot step per CC, showing the plateaus where a change in CC had no impact.

What Rolls Down Stairs?

    Now let's think about what curve would help fix up our taper. We need to reduce the lip at the bottom, and smooth the spike at the top by progressing through those more slowly. The middle is a little unresponsive, so we can speed through that to balance things out. This slow-on-the-ends curve is called an s-curve.

    One of the simpler s-curve functions is the logistic function that takes this form:

`L/(1+e^(-k(x-x_0))`

    `L` scales the function vertically, while `k` sets the severity of the curves, and `x_0` defines the midpoint. As in previous posts, we want to normalize the curve to a range of 0-1, so we can make `L = 1`. We want the curve roughly centered in our range, so we'll let `x_0 = .5`. `k` we'll have to play with so we don't end up with too shallow of a curve.

    Just like last time we can make the function pass through (0,0) by subtracting the y intercept. We can also force it to pass through (1,1) by dividing by the value of `y` when `x = 1`. By doing both we have a normalized function that lets us experiment with values of `k`.

    Here's a first guess of `k=10`. I've added short line segments that show our minimum angle of x/2.

logistic curve where `k=10`

    This is too shallow to resolve on the digipot without wasting CC steps. By adjusting `k` until the ends hugged the bounds we've set, I settled on a value of 5.2.

logistic curve where `k=5.2`

    By taking the result of this function, and plugging it in for x in our old log-approximation formula (blue), we can see our new potentiometer taper in orange, compared to a more idealized curve in black

compensated taper vs original approximation vs "ideal"

    The black line is a target curve I came up with arbitrarily. I used the form `n^x`, normalized it, and picked a value of `n` that passes through the original curve at `x=.5`. You can see the compensation got us maybe halfway to the target. It's not great, but it's a free gain in accuracy given we're just being smarter about how we use our potentiometer.

Everyone Wants a Log

    How does this work in practice? It's a noticeable improvement, but it's not ideal. The ends are still jumpy, and the middle isn't terribly responsive. We can improve the resolution of the digipot by combining two digipots as if they were one. If we put them in series we get twice the steps, plus we gain the ability to adjust the top and bottom separately, giving us many more ratios of resistance. Still, we're chasing this log taper using low-resolution linear means, and that's something of a losing proposition.

It's Big, it's Heavy, it's Wood

    The real solution is to treat the LFO like the VCO, and give it the exponential current source that it deserves. There's one big difference though: the VCO is a saw core, and the LFO is a triangle core. This means we have to not just sink current from it, but source current to it. How do we make our current source bidirectional? 

    Wikipedia defines the Operational Transconductance Amplifier like this:

...an amplifier that outputs a current proportional to its input voltage. Thus, it is a voltage controlled current source.

    Crucially, when the input voltage goes negative, so does the output current. That's just what we need. There's a second current input (pin 1) that defines the magnitude of the the output by multiplying it with the voltage input. We can hook up our single-direction exponential current source here, while using the voltage input to change the direction of the output, thus controlling the triangle. 

Exponential voltage control for the LFO

It's Good

    This give a much better response than misused linear pots. Notice we had to change the timing cap, C24. It was simply too large, and required the OTA to push excessive current for higher LFO rates. We can now achieve both much higher, and lower frequencies than before, despite the 10x smaller cap. The CV input just needs to be hooked up to a microcontroller's DAC, or anything else that can produce a usable voltage.

Fits on Your Back

    We spent so long investigating the math of the approximate log taper that it'd be a shame to do nothing with it. There is another spot in the circuit where they appear to use the same trick, and it also relates to the LFO. It's the LFO Depth adjustment.


LFO depth log approximation

    We can tell from the ratio of the pot value to the resistor that this is a much more gentle curve than the LFO rate pot. That means we have a better shot at being able to compensate for it. We can plug the new values into our existing curve and see the taper in blue. By playing with the values of the s-curve we can match the compensated curve (orange) to our target curve in black.

LFO Depth curve. Before: blue, after: orange 

    The compensated curve comes quite close to target line. We can implement this in code using the same precalculation trick from last time. 

float normVal = CC / 127.0;

constexpr float x = 0.51; //sets the middle of the curve on x-axis
constexpr float k = 3.8; //sets the curve severity
//precomputed offset and denominator
constexpr float offset = 0.125867742017; // = 1 / (1 + exp(-k * (0 - x)));
constexpr float scale = 255 / (0.865529894061 - offset); // = 1 / (1 + exp(-k * (1 - x))) - offset;

//apply s-curve formula
float sCurve = 1 / (1 + exp(-k * (normVal - x)));
sCurve = (sCurve - offset) * scale;

    We still have to do one costly call to exp(), but we've optimized out two more that would have been in the calculations for offset and scale. We've also avoided a couple extra division operations.

    The precalculation could be taken further to work out all 127 CC values ahead of time, or we could change the formula to exactly offset the log inaccuracies, but I don't think there's a lot to be gained there. 

No comments:

Post a Comment