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
D0P3014B
D1P3024A
D2P1051A
D3P1041B
D4P1032A
D5P1022B
D6P1060B
D7P1070A
D8P3047A
D9P3037B
D10P1123B
D11P1091A
D12P1101B
D13P1113A
A0P014-
A1P000-
A2P001-
A3P002-
A4P1015A
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.

Wednesday, April 1, 2026

Gakken, Portamento Concepts, and Euler (fixed rate/time, linear/exponential glide)

     Last time we squared away the tuning for our Gakken SX-150 mk II by using Rossum compensation and a basic autotune formula. Now we're free to add more features. Today we'll look at glide/portamento.

SX-150 under Arduino R4 control

Fundamentals

    Portamento is a smooth transition from one note to another. Some instruments lend themselves to this, and some don't. A slide-whistle can very easily slide between notes, but a piano cannot.

    Our Gakken oscillator now tracks over a wide range, and has no limitation on what frequencies it can play within that range. We're more so limited by the DAC and its resolution. We won't worry about that today though.

Movement I (Fixed-Rate)

    So, we need to move smoothly from note to note. Simple enough. We can pick a rate (in terms of note per second) and progress by that over time. Since we're updating a DAC, we have to take finite steps at finite intervals, but we'll think of it as continuous.

    We'll start simple with a rate of 1 note per second. If we go from, say, B to C (1 note), it takes one second. If we go from B to B up an octave, it takes 12 seconds. This is a natural result of using fixed-rate portamento, and it can be a nice effect that accentuates large leaps in pitch. On the other hand, it makes these pitch changes sluggish and limits what notes you can play within a given timespan.

Fixed-rate portamento taking longer for larger intervals

Movement II (Fixed-Time)

    The opposite of fixed-rate is fixed-time portamento. It starts with a timespan (we'll stick with 1 second) and fits the glide within it. Whether your interval is 1 note or 5 octaves, it will still take 1 second.

    The implementation is simple: find a rate that will travel the interval in the given time. If our interval is 12 notes, and we have half a second, a rate of 24 notes per second will satisfy the requirements. It's our favorite middle-school-math again: rate = distance/time 

`r = d/t`

Fixed-time portamento taking the same amount of time for large intervals

    This solves the issue with fixed-rate portamento, but we lose the ability to accentuate those large intervals. We can have the best of both worlds by taking a weighted average of the two. By changing the weight, we can get an arbitrary blend of the two responses. Let `A` = fixed-rate, `B` = fixed-time, and `w` = weight 

`A*w + B*(1-w)`

    We can do a little algebra to get a version that only requires one multiplication from our processor.

`A-w*(A-B)`

Average of fixed rate and time portamento

A Secret, More Complex Third Thing

    The previous two methods outline portamento's relationship to time, but we haven't looked critically at the other axis: pitch. Our pitch doesn't have to travel in a straight line over time. It can move along a curve. There's a specific one that's very popular due to how portamento is typically done in the analog domain: the RC curve. If we put out pitch CV through a simple RC lowpass filter, we will have portamento that follows an RC curve. Great, but what's the advantage of that, and how can we do it digitally?

An RC curve from michaelsharris.com

    The advantage is subjective, but many people simply prefer the sound of the pitch taking off quickly and settling more slowly. It's described as more "musical". It's also likely a familiar sound. It makes me think of the "Ironside siren" that featured in the Kill Bill films. 


    We can look at the spectrum of this siren to confirm that it is an RC curve.

Ironside Siren RC curve

    Let's see how we can approximate this curve with code. We'll start with the typical RC equation. `V` is the voltage applied, `R` is the resistance in ohms, `C` is the capacitance in farads, and `t` is the time elapsed in seconds. We finally dip into high school math to get our friend Euler's number: `e`

`V(1-e^(-t/RC))`

    Here that is graphed in Desmos. R and C are both 1, while V is 5. Notice that, despite getting very close, the value doesn't reach 5. In fact, it never reaches exactly 5; It just gets closer and closer as time elapses. While not a show stopper, this can cause funny problems once DAC and data type precision has to be factored in.

`V(1-e^(-t/RC))` (RC = 1, V = 5)

    We can use this equation, keep track of time, and plug it in for `t` when we want to update our portamento. It's not an equation that's particularly friendly to our microcontroller though. It involves division and raising a float to the power of another float. 

Getting ahead of the curve

    In this project we're using an Arduino R4 that's a large step up from the standard Arduino we've been using. We have a 32-bit architecture and a floating point processor, so we don't need to go to the optimization lengths we did previously. No more lookup tables, or fixed-point math. Still, let's look for a less expensive way to generate this curve. We'll start by trying to understand the formula via free association.

Easy `e`

    When I see Euler's number (lil' e as my professor called it), I think of compound interest. The compound interest formula is as follows:

`A=P(1+(r/n))^nt`

    If we plug in ones for all variables but n, then simplify it, the formula takes this form, where n is how frequently we compound.

    `(1 + 1/n)^n`

    As we compound more and more, this function approaches the value of `e`.

`(1 + 1/n)^n` approaching `e`

    The formula for continuously compounding 
interest plugs `e` back into the periodic formula. It has the starting amount: `P`, multiplied by `e` to the power of the interest rate: `r`, multiplied by the timespan: `t`.

`Pe^(rt)`

    This does relate to the RC curve. The starting value is the complement to our starting Voltage, the rate is equivalent to the rate set by RC, and time, as ever, is time. In fact, the curve is the same, just rotated. We can spin and overlay our previous RC curve onto a compounding interest curve (`Pe^(rt)`), and see that they match up nicely.

Comparing rotated RC and interest curves

`e` for Effort

    We took a little detour looking at `e`, but it brought us to compounding interest. As we saw, this can be continuous or periodical. Our RC formula is most similar to the continuous formula, but the digital domain is never truly continuous. We always have to take a series of steps, so maybe a periodic approach would be a better fit.

    Instead of using a formula that calculates our interest at any given point in time, let's think of interest as a process. We start with an amount: `P = 1`, and maybe monthly we add our interest: `r = 10%`. The calculation for the first month will be this: 

`P + (rP) = 1 + (.1) = 1.1`.

    The following month we're not starting from our principal of 1 anymore; We're instead gaining interest on 1.1, and so on for however many months.

   `1.1 + (.1 * 1.1) = 1.1 + (.11) = 1.21` 

    A code equivalent might be: "total += total * rate". Isn't that nice and simple? No exponents nor division to worry about.

    If we graph the result of this (orange), we can see that it's a very similar curve to the continuously compounding curve (blue).

total += total * rate vs `Pe^(rt)`

    In fact, this is the same kind of exponential curve, only not as steep. This is simply due to us compounding less frequently. If we compound 10 times more often, the resulting curve is a much closer fit.

compounding at 10x

    This is no coincidence; This approach is called: Euler's method. It's an iterative process of adding and adjusting values to linearly approximate curves. It's so simple and useful that it pops up in many different places, such as modeling population growth, estimating heat loss over time, or in applications of the "Lerp" (Linear Interpolate) function that's ubiquitous in graphics and animation.

The Larch Lerp

    The Lerp is a simple linear interpolation between two values, and it takes a slightly different form: `a + (b - a) * t`. `a` and `b` are the values being interpolated between, and `t` is a value from 0-1 that represents how much we want to progress between them. Looking at the formula, it only serves to draw straight lines between points. The beauty comes from how it can be called within a program. 

    Let's say `a` is the position of an object, like a spaceship in a game, and `b` is a planet that it's approaching. We can lerp their two positions every frame, and use this as the new position for the spaceship. If we use 10% (0.1) for `t`, the ship covers 10% of the distance to the planet every frame. As it gets closer, 10% of the distance becomes less and less actual space that it covers, so the ship slows. Since this is based on a percentage of the distance, it takes the same amount of time regardless of the actual distance (fixed-time). Here's a gif of exactly this, from a very nice article on lerp and "easing".

using lerp to move an object - from erraticgenerator.com

    So, we're getting diminishing returns as we iterate, and we're converging on a target value: `b`. This sounds very much like our RC curve. If we graph our distance covered on the Y axis, it looks very much the same too.

`y_1 = y_0 + (b - y_0) * t`

    Finally we can write this in code as: 

total += (target - previousTotal) * rate

That's it. One simple line of code that will give us our RC response.

Bringing it full cycle

    We can also think of that last formula as: distance_remaining * rate + total

     This might not seem significant, but it's the backbone of many DSP functions. It's so vital that dedicated DSP chips include a Multiply-Accumulate (MAC) instruction in their hardware that takes the form of: `A * B + C`. This is commonly used to implement things like highpass/lowpass audio filters.

    Now remember that the RC portamento circuit we're modeling is really just a lowpass filter, and what we're doing in software is actually DSP code pretending to be a resistor and capacitor.

"Not the wind, not the flag; mind is moving"