Saturday, August 1, 2026

Adding waveforms to the Gakken (Simple Saw Animator)

     As part of our ongoing series on the Gakken SX-150 mk II, I've been thinking about various ways it can be enhanced. One obvious shortcoming is the complete lack of waveform options. We have a saw; that's it. There's no second oscillator, nor sub oscillator.

Hip to Be Square

    A simple addition is a square waveform. All we need to do is compare the saw to a midpoint voltage. If the saw is above the midpoint, the output is high, else it's low. This raises the question: what is the midpoint? We can find this empirically, by measuring the waveform with an oscilloscope, but there's no guarantee a different Gakken will have the same midpoint. The midpoint might also drift with time, or old batteries. It's best not to build the circuit around an unknown like that, so let's remove it by adding a highpass filter and rebiasing the saw.

Saw to Square conversion

    There it is, rebiased around 2.5V. If we'd like to change the width of the pulse, we can vary the voltage at the inverting input of the opamp.

Pulse width modulation

    So, we get Pulse Width Modulation (PWM) for free, just by providing a voltage. We do have to be careful to match the range of the CV to the amplitude of the saw, or the PWM range won't be what's expected.

You're Thinking of Someone With Two Knives

    Wouldn't it be nice if we had a second oscillator to thicken up the sound? Well, maybe we can fake a second saw wave. Back in 1978, Bernie Hutchins published a "waveform animator" that does just that. 

    It works by comparing a saw (or triangle) waveform to a fixed voltage, then summing the saw and the output of the comparator. The comparator output shifts a chunk of the saw up, and the rest of it down, in a way that effectively changes the phase of the saw. It's as if it moved forward/backward in time relative to the original, just like a second saw would do if it were a slightly different pitch.

Fig 2 from Electro Notes #87

    It's a really great trick, and it only has the small catch that the resulting waveform now has a DC offset, due to the comparator output being lopsided. This is accounted for by mixing in some of the CV that drives the comparator.

From All Angles

    Now that we know it's possible to fake a second saw, we can riff on the idea, and maybe make something that's more in the minimalist spirit of the Gakken.

    The original design mixed an equal proportion of the comparator's PWM output with the saw to get the phase shifted saw. We don't have a use for it by itself, and we really just want the mix of the two saws. Turns out we can get the same result by just dropping the amplitude of the PWM signal. It yields the same proportion of phase-shifted saw to original saw.

    Here's a comparison between two actual saws and our half-amplitude-PWM circuit:

Two saws vs one saw + PWM

    Neat, huh? The PWM signal (red) slides a segment of the saw upwards to represent our phantom, second saw. This has saddled us with the DC offset problem discussed earlier, but we have an otherwise perfect emulation of two saws. 

    We can see the waveform starts with a full amplitude saw that breaks into two uneven peaks. Those peaks then level out into a saw that's twice the frequency but half the amplitude, then the process reverses.

Two Are Better Than One

    The original wave animator circuit was meant to be duplicated multiple times to get additional shifted copies of the wave. There would be a new LFO, inverting amp, comparator, and summing amp per copy. That's a bit excessive for our little Gakken, but we can take inspiration from the idea.

    Mixing in one PWM signal gave us a second saw, so maybe we can tack on another PWM signal to get another saw. If we invert the phase of our existing PWM to make this "new" PWM signal, it'll simply cancel out in the mix, returning us to a single saw.

    If we invert the saw instead, we get a different PWM signal, one with pulses that "grow" in the opposite direction and offset a different portion of the saw. If we make sure it has the same pulse width, but opposite polarity of the first PWM signal, it will even cancel out the DC offset that we introduced.

PWM #2 generated from an inverted saw

    We can see from the "PWM Mix" signal that the combination of the two signals is balanced around a midpoint with an even amount of time spent above and below.

    If we mix in a specific amount of the inverted saw, we can see the multi-saw waveform emerge, this time with an additional peak.

The full saw animator

    There we have our full saw animator built around just 3 op-amps. The fourth is left as an optional buffer.

There's What's Right and There's What's Right

    Clearly it's easy to add square waves, so let's come up with another one. Adding a square sub oscillator is an obvious DIY move. All we need is some sort of counter and a reasonable pulse to feed it, then out comes a square wave an octave down. The counter is performing a divide by two operation on the number of incoming pulses. One pulse out for every two in. This amounts to a divide by two operation being performed on the frequency of the incoming pulses also.

Adding a d-type latch to generate a sub octave

    We tap off of one of our PWM signals so that we can get a clean pulse for our counter. Using an inverter keeps the sub aligned with the falling edge of the saw, rather than the other edge of the PWM pulse. This is because the counter only counts on the rising edge of the clock.

Never the Twain Shall Meet

    Much like the saw, things get interesting when we mix our waveforms together in specific ratios. Here's the sub mixed with both PWM signals.

Pulse and sub mix

    Look at that. We get a new waveform, a stepped saw that's an octave down. This is similar to the Korg Poly-800's saw that it builds from different octaves of square waves.

Submerge

    Now that we have our square sub oscillator, we can combine it with our saw to get a triangle sub oscillator. All we have to do is invert the saw when the sub is low, giving us two back-to-back right triangles that form an isosceles triangle.

    We already have an inverting amplifier that gives us an inverted saw. We could use an analog switch to toggle between the inverted and non-inverted saws, but the non-inverted one isn't buffered and would get rebiased as we connect and disconnect it.

    We can instead borrow a trick used in attenuverters. Rich Holmes has a nice breakdown of how they work here, and we'll borrow one of his images:

Simple attenuverter - richardsholmes.com

    We can imagine the pot with the wiper at the bottom (counterclockwise) extreme. The non-inverting input (pin 3) of the opamp is shorted to ground, and we get an inverted output. At the other extreme, pin 3 instead sees the full input voltage, and we got a non-inverted output.

    We can use a SPDT switch to do the same thing: connect the non-inverting input to ground or the saw wave, alternately. This input pin pulls virtually no current, so it doesn't upset the bias of the saw.

sub triangle wave

    There is a small hiccup where the saw is inverted, but this is typically inaudible due to how narrow it is.

    Now we have a saw, two pulse waves, a square sub, and a triangle sub. Depending on how we mix them, we can also get two flavors of animated "super" saw (one PWM or two), a stepped sub saw, and a regular sub saw. It's worth pointing out that the triangle sub conflicts with the animated saw, since it robs us of the inverted saw half the time. The combination does still produce an interesting, half-animated triangle though.

Flashing WeAct Studio RA4M1 with the Arduino R4 bootloader

     I've been using an Arduino R4 minima in my Gakken SX-150 project, but I ran out of IO pins. The RA4M1 IC itself has plenty, but only a handful are broken out on the Arduino board. Luckily, there's an alternative board that exposes nearly every IO pin, the WeAct Studio RA4M1.




    The board is pretty spartan, adding little more than a few LEDs, buttons, and a voltage regulator. This gives us the essentials without robbing us of the extra IO we're after. One problem though, it doesn't have out-of-the-box compatibility with the Arduino IDE, despite sharing the same microcontroller.


Arduino Compatibility

    What makes an Arduino an Arduino, and what is this board missing? In a word, the bootloader. The Arduino R4 boards come preprogrammed with a bootloader that allows them to talk to the IDE, and load new sketches. Fortunately, this bootloader is freely available and can be flashed onto the WeAct board.


Setup

    First we need a tool that lets us program the RA4M1 chip. This is imaginatively named "Renesas Flash Programmer", and can be found here: https://www.renesas.com/en/software-tool/renesas-flash-programmer-programming-gui

    Next we need the Arduino R4 Minima bootloader from here: https://github.com/arduino/ArduinoCore-renesas/blob/main/bootloaders/UNO_R4/dfu_minima.hex

    Then connect the WeAct via USBC. It should start flashing a blue LED. This is the "blink" program that it comes with. This is a good sign, and shows that the board is working. We need to stop it though, and get into programming mode. This requires a "secret" button sequence: hold down the "MD" button and press "RES". If done correctly, the LED will stop flashing.

    Now open the Programmer program and select "RA" for the microcontroller, and "COM port" for the tool. It should automatically detect a com port and display it to the right of the "Tool Details..." button.


    Verify that the port is detected as an RA Boot device



    Click the "Add/Remove Files..." button and select the hex file we downloaded


    Click the large "Start" button and wait for confirmation that it was successful.


     Done!


Using the Bootloader

    Now that we have the bootloader, we can use the board as if it were an Arduino R4 Minima. It should appear in the list of boards as an R4 Minima, and respond to programming the same way.  



The Catch

    If you upload the standard "Blink" sketch via the Arduino IDE, you'll find that it doesn't actually blink (flash the LED). This isn't due to any bootloader or firmware problems; the LED is attached to a different pin than on the R4.

    While the microcontroller is compatible with that of the R4, the board is different. The IO pins don't adhere to the Arduino D0-D13/A0-A6 numbering, and many of them aren't supported by the traditional Arduino pinMode()/digitalWrite()/digitalRead() functions. The pin are still very much usable though!

    One solution, offered by WeAct, is to use a new set of functions: R_IOPORT_PinCfg()/R_IOPORT_PinWrite()/R_IOPORT_PinRead()

    These are demonstrated in their "Blink.ino" sketch on the WeAct github: https://github.com/WeActStudio/WeActStudio.RA4M1_64Pin_CoreBoard/blob/master/Examples/Arduino/Blink/Blink.ino


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 a day, right? Well, yes, but the original pot isn't doing its 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 gives 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. 

Monday, April 6, 2026

Gakken Envelope Modifications and MIDI

     As part of a series on the Gakken SX-150 mk II, we're looking at the envelope. Much like the rest of the synth, it's very spartan. Our only controls are attack and decay, making it an AD envelope. We could add MIDI control and call it a day, but wouldn't it be nice to have a full ADSR?

The stock SX-150 mk II envelope

    This is a pretty standard design. An incoming gate signal gets filtered into a trigger that sets a latch, starting the attack phase. The latch charges a cap until it passes the threshold of a comparator, and the comparator resets the latch, starting the decay stage. The capacitor discharges through the latch until another gate signal is encountered.

All the World's a Stage

    The latch state defines the stage, and it only has two states, so how do we add a stage? We need to fold in another signal to help define it. We have the input trigger, but that's too fleeting. What about the incoming gate? The rising edge of it lines up with the start of the attack, so that's not adding any information. The falling edge can extend past the end of the attack stage though; That's something new to work from.

    We can use the gate signal to "lift up" the decay stage, so that it decays to a sustain level instead of 0V. We'll sever the connection between the latch and the decay pot, then connect it - via potentiometer - to the gate signal. The potentiometer lets us set the sustain level.

Envelope response with sustain adjustments

    Well, it sort of works, but the attack stage gets rounded off and lowered as we lower the sustain. If the sustain gets too low, the attack stage is unable to progress to the decay stage. Previously, the latch kept the left side of the decay potentiometer high (~5V) during the attack stage. This combined with the diode prevented it from discharging the capacitor. Now we're setting that side to the sustain level, and fighting against the attack stage. The resistance of the sustain pot is also making the decay longer, and limiting the minimum decay time. Let's fix it.

Self Sustaining

Sustain envelope V2

    That's more like it. The attack stage isn't impacted, nor is the decay rate. This is because we mixed the attack signal into the sustain level (seen in red), temporarily raising the sustain level and preventing it from pulling down the attack stage. We also added a buffer to prevent the sustain pot's resistance from changing the decay.

    We even gained a more elegant way to turn off the sustain: a single-throw switch instead of a double. We can also get the same effect by turning the sustain level down to 0V.

Release Yourself

    Now we have an elegant way to add sustain, but notice that the decay and release rates are the same. That's because they're defined by the same pot. How can we separate out the release rate?

    We need to repeat the trick we just learned, and prevent the decay stage from pulling down the release stage. If we know when the release stage starts, we can force the sustain level high during it. Well, that's pretty easy; The release stage starts as soon as the gate goes low. The circuit already has an inverted version of the gate that we can tap into, and diode-or with the sustain level, bringing it high.

    Now that the decay stage is out of the way, we need to do the same for the release stage. We can pipe the non-inverted gate signal into it, keeping it held high and inactive until the start of the release stage. Once that goes low, the release stage can start, draining the capacitor through a new, separate release pot.

Full ADSR

    There we have it, a full ADSR that only requires six extra components.

MIDI Control

    We have our improved envelope, how can we control it with our Arduino R4? Digipots are a natural choice since they're drop in replacements for the original potentiometers. There are drawbacks though. Their resolution is finite, usually only 128-256 steps, and they can't pass high current. This envelope uses an especially high value capacitor at 100uf, and this takes a large current to charge/discharge quickly. Let's lower the capacitor value so we can reduce the current demand. 10uf is a little more reasonable.

    Lowering the capacitor shortened the time of the entire envelope by a factor of 10. We can compensate by increasing the potentiometer values by 10. There's a problem though; Digipots only come in a few values, usually topping out at 100k.

Finger on the Pulse

    Another option that we've looked at before is PWM and analog switches. PWM has the potential for many more steps (65536 with full 16-bit PWM). The switches pass more current, and they have a large range of effective resistances. The main drawback is that they don't pass high frequencies all that well. This isn't a problem for our envelope though.

    There's a native library that makes it easy to setup PWM on the R4. We want to select a pin that doesn't interfere with things like SPI, and we want to avoid clobbering any timer that we're already using (like GPT0 used in our prior autotune code).

    Finding the relationship between Arduino pin, RA4M1 pin, and GPT channel requires cross referencing multiple documents (and the Nano R4 User Manual is currently incorrect!), so here's a table:

Arduino PinRA4M1 PinGTIO
D1P3014B
D0P3024A
D2P1051A
D3P1041B
D4P1032A
D5P1022B
D6P1060B
D7P1070A
D8 P3047A
D9P3037B
D10P1123B
D11P1091A
D12P1101B
D13P1113A
A0P014-
A1P000-
A2P001-
A3P002-
A4P101 5A
A5P1005B
A6P004-
A7P003-

     Pin 5 fits the bill, corresponding to GPT2, so we can call the library like this:

#include "pwm.h"

PwmOut pwmPin5(5); //GPT2
pwmPin5.begin(10000.0f, 0.0f); //10kHz PWM
pwmPin5.pulse_perc(10.0f); //set duty percent

    That gives us 10kHz PWM with a 10% duty cycle. Varying the duty cycle changes the effective resistance, and sets the attack/decay time. The relationship between duty cycle and decay rate is not what you might expect though, and the majority of the adjustment happens in the range of 0-10%.

    We can analyze the effective resistance of the analog switch under PWM, and the response of the RC circuit, or we could just tweak some curves until it feels right. Given that all we care about is the feel, let's do that.

Getting to the Bottom of Things

    We can normalize our MIDI CC value so that it spans 0-1 by just dividing it by the max value of 127. We'll let `x` represent this value and that'll make all the math simpler.

    Since our range is all bunched up at the lower values, let's try making our CC exponential. An exponential curve rises slowly, and that serves to space out the lower values, while bunching up the higher values instead.
    We can pick a base number: b, and take it to the power of our CC value, `x`: `b^x`

`10^x`

    Here's what `10^x` looks like. The curve is ok, but there are problems. Most of it is defined by negative values of `x`, it never hits 0, and the y-intercept of 1 isn't very convenient.

Made to Fit

    The y intercept of 1 holds true for any base value, because of the definition: `b^0 = 1` (when `b != 0`). We can force any formula to pass through (0, 0) by just subtracting the y-intercept:

`b^x - b^0 = b^x - 1`.

`10^x - 1`

    We're anchored at (0,0) now, but that's about it. If we can force it to pass through (1,1), then we'd have a very handy normalized function (one where `0<y<1` for `0<x<1` ). We can plug in an `x` value of 1, and evaluate for `y`: `y = b^1 - 1 = b - 1`. This is another special case for exponents where `b^1 = b`.

    We know that anything divided by itself gives us 1, so we can take the `y` value we just found for `x=1`, divide the whole formula by it, and that will force it to output 1 for `x=1`: 

`(b^x - 1)/(b-1)`

`(10^x - 1)/(10-1)`

    There we go; The curve passes through both points (0,0) and (1,1). Now we can adjust `b` and only change the severity of the curve, not the range.

`(10000^x - 1)/(10000-1)`

    Finally we can rearrange the math to make it a little more friendly to the R4. Division is slower than multiplication, so we want to help the compiler avoid it. If we pick a fixed value for `b`, we can precompute the reciprocal of the denominator, turning the division into multiplication. Sometimes the compiler is able to do this itself, but we can spell it out without impacting the legibility of our code.

    While we're at it, we can scale from our 0-1 range, up to 0-100 for the duty cycle percentage by multiplying our precomputed value.

constexpr float b = 50000.0; //curve severity
constexpr float r = 100 / (b - 1.0); //precalc reciprocal
float expoVal = (pow(b, normalizedCCval) - 1) * r;

pwmPin5.pulse_perc(expoVal); //set duty percent


Sustained Fire

    We can apply this scaled-PWM-analog-switch solution to the attack, decay, and release times. The sustain control is a different animal though. It's set up as a voltage divider instead of a single resistance, it doesn't have to pass a high current, and it doesn't need to be adjusted exponentially. This is actually a great use-case for a Digipot... but PWM channels come in pairs on the R4, and we've used three. Let's rework the sustain level to use this free PWM channel.

    If we lowpass the PWM signal, we get an equally "free" analog output. The voltage we get is proportional to the duty cycle. Filtered PWM can be a little sluggish to change, but this isn't a problem for a rarely-changing sustain level.

    Remember that we switched out the decay pot for an analog switch. It can do double duty, connecting and disconnecting our sustain CV as needed. A little more diode logic will do just that. We add a simple filter before the op-amp buffer, and we have a PWM-driven sustain stage.

Filtered PWM sustain

    Again, there are multiple ways to solve this problem, and the best option is going to depend on what parts/IO we have to spare. If we have a leftover CV channel, that's better than filtered PWM. If we have a spare digipot, why not use that? We'll reevaluate our options as the rest of the MIDI-fied Gakken project progresses.