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"

Friday, March 20, 2026

Gakken SX-150 mk II Tuning (Rossum Compensation)

     Last time we looked into tuning the oscillator of the Gakken. We assumed a perfect exponential response though, to simplify the concepts of autotune. The real hardware is far from ideal though.

The Gakken SX-150 mk II being subjected to tuning experiments

A measured response

    I wrote a bit of code that tests the autotune accuracy by playing each note, measuring the frequency, and logging it. I put the results into a sheet, and calculated how much each note is out by. As a rough reference point, 10% and below is acceptable. Deviation above that will likely be noticeable to the average listener.

Tuning baseline


    Well, that's pretty bad. The last third or so of our range is well out of spec (10% shown by the red line).

The core issue

    Notice that the notes go flatter and flatter as we go up the scale. The oscillator is a saw-core, and these have a quirk that can cause them to go flat on high frequencies. This is due to the time spent "resetting" the waveform. This takes a larger percent of the cycle as the oscillator plays higher frequencies. The real time spent can also be longer, due to the reset signal "fighting" against the stronger pull of the expo converter at high pitches. Here's an exaggerated example:

Saw-core going flat

    We can see the upwards slope of the lower frequency saw takes about 7 division, and the fast one takes about 3.5. This should mean that they're an octave apart. We have to add in the downward slope that is the reset time though. It's about 1.5 division for the low note, and almost 2 for the high one. This completely throws off the octave relationship and gives us 8.5 vs 5.5 instead of 7 vs 3.5.

Fair compensation

    Our actual problem seems to be in the exponential converter though. Rene Schmitz has a great article on these converters, and how to compensate for these kind of high-frequency problems. He explains that the problem is caused by the effective resistance between the base and emitter (rBE) of the transistors used. This figure varies, and is rarely spec'd in datasheets, so it's hard to say what we're up against in the Gakken.

    There is a fix for high rBE though, and it's called Rossum compensation (named after synth legend Dave Rossum). It involves feeding some of the op-amp's output current back into the converter's input. All that's needed is a diode, a resistor, and a trimmer. I used what I had available, and found that a 5k trimmer and 470k resistor work reasonably well. Here they are added into the schematic:

Gakken with Rossum compensation

    So, how much has this improved the high frequency problem we had? Let's dial in the trim pot and rerun the test. No adjustments need to be made to the autotune code.

Compensated tuning

    Look at that; The high frequency issue is gone! This suggests that high rBE is a problem in the transistors used.

    The tuning still isn't flawless, but we've addressed the issue and the whole 7 octave range is under our 10% figure. We could have compensated for this in software, and we may still, but I think this is an impressive result for such a small modification.



Monday, March 16, 2026

Gakken SX-150 mk II & MIDI Autotune fundamentals

     I'm a big fan of small, toy synths and today we have the decidedly chintzy Gakken SX-150 mark II. It's the follow up to the SX-150 that was sold with the July 2008 issue of Gakken magazine.

    There is a pre-production schematic for it, but I don't love the layout. As for board diagrams, there are none. So, I of course redrew it here.

Red-headed step ladder

    There's not a lot to the synth, but the filter is interesting. It's a diode ladder filter (made from transistors) that closely resembles the 303's.

TB-303 filter

SX-150 mk II filter

    The biggest departure comes right before the output. The 303 uses a differential pair (Q21), but the Gakken replaces it with a differential amplifier (IC1B). The original transistor pair is actually a matched pair contained in one ic: the 2SC1583. This part is now rare and expensive, so they redesigned around the extremely common "jellybean" LM358 op-amp.

    They let the other two matched pairs (Q22 and Q12) simply be separate transistors. Maybe they selected for close matches, but I'd guess they didn't, given the price point.

MIDI

    It's not much of an instrument as it is, since it's very difficult to play any specific notes. The only interface is a stylus and a resistive strip that you touch it to. Let's add MIDI instead.

    The combo of stylus and strip forms a voltage divider that yields values in the range of 2.5 to 5V. How exactly does this control the oscillator though? Let's look at the schematic.

Gakken Oscillator

    Well, it has an exponential converter. That's very convenient for us. We can send our control voltage in via the "Strip" node. This is the point at which the stylus and the resistive strip meet. We can connect to this by alligator-clipping to the stylus tip itself. But, t
here's a snag. 

Gate binds you

    The "Gate" signal is derived from the Strip voltage. If the Strip voltage dips too low, the Gate goes low and disables the oscillator. This feature exists to prevent the VCO from droning when no note is being played (because there is no VCA!).

    If we just remove R5, this decouples the pitch CV from the Gate signal. We can then use the "Strip" input to inject a gate signal instead, and connect a new resistor to the CV summing amplifier. What resistor should we use though? Well, let's look at the gain of the amp.

No pain, no gain

    The original input resistor is 1M (R5), and the feedback is 470k (R21). `470000 / 1000000 = .47`. This gain is a bit high to be driving an exponential converter, but the value of feedback resistor is very high. This makes it easy for noise to to creep in and impact the pitch of the VCO, especially when experimenting using breadboards and jumper wires. So, let's reduce R21 to 47k.

    Just from playing with the values, we can find that 120k gives a nice, wide pitch range. The gain is very similar to what it was before (.39 vs .47). It turns out it needs to be relatively high due to the voltage divider formed by R25 and R30 that lowers the gain down to .025.

Autotune

    Now that we have a way to control the VCO, what voltages do we need to send to play a musical scale? That's going to depend on the exact value of components in a given SX-150, along with temperature (to some extent). We can add trimmers to hand adjust it, but we can instead make a microcontroller do the work for us. Previously we looked at a proof-of-concept guitar tuner made with an Arduino Nano R4. We can build on this for our autotuner.

Music is Math

    The tuner code can only tell us what a given pitch is, but that's a good starting point. We can send a voltage to the VCO, measure the frequency, and then repeat with a different voltage. This gives us two points of data, and that's enough to tell us where all the notes lay (assuming a perfect exponential response). This is because only two points are needed to define the line that they fall on.

    To make things simpler, we'll be converting frequency to note number. Here's a formula that does that:

`12 * log_2(f / 440)`

    Here are two real world values that I got from one version of my tuning code. It tried to play note #12 and got note #-9.81. Tried to play note #36, got note #47.95. This gives us points (12, -9.81) and (36, 47.95). We can use our favorite middle-school-math™ to find the definition of this line, revealing the location of all notes. Remember slope-intercept form? `y = mx + b`

    The slope (`m`) is defined by the rise (change in `y`) over the run (change in `x`):

`m = (y_2-y_1)/(x_2-x_1) = (47.95-(-9.81))/(36-12) ~~ 2.41`

    The other component is the offset (intercept/`b`). We can get this by plugging one of our pairs into the equation, along with the slope that we just found.

`y = mx + b`
`47.95 = 2.41(36) + b`
`47.95 - 2.41(36) = b`
`-38.81 = b`

    We can graph this with Desmos, and see that the line: `y = 2.41x-38.81` does pass through our two points, and the intercept is at -38.81.

Desmos graph of `y = 2.41x-38.81` 

    Now we've worked out the relationship between the note that we request and the note that we ultimately get. How do we adjust the note we request so that we get the note we want though? We have to work out the opposite line that will compensate for the existing one. The inverse slope will fix the scaling, making a change of 1 note on the input cause a change of 1 note on the output.

    It's worth pointing out that we can't change the original line's formula (without changing the hardware somehow); We can only modify our value of `x` that we put into it. So, here's `1/2.41x` plugged in for `x`, giving us:

 `y = 2.41(1/2.41x)-38.81`

    Notice how the slope is a nice 45 degrees now, but it still starts on the very odd value of -38.81.

`y = 2.41(1/2.41x)-38.81` in green

    What value could we add that would make our curve start from 0? It's tempting to say 38.81, to offset our -38.81, but that's not quite it. Remember the value will be multiplied by the original slope of 2.41. So, we need to again compensate for the slope and do: 

`-b/m = -(-38.81)/2.41 = 16.10`

    We can combine this with our slope, and rearrange terms so that it's more clear this is the inverse of the original line:

`y = m((x-b)/m) + b`

`y = 2.41((x + 38.81)/2.41) -38.81`

`y = 2.41((x + 38.81)/2.41) -38.81` in green

    The Arduino can easily perform this math, and Tada; We have a synth that's in tune.

    All the program has to do is find `m` and `b`, then plug them into this formula: `(x-b)/m` where x is your note.

    In the future we may explore the tuning of non-ideal oscillators. In the real world you'll find that the SX-150 goes noticeably flat in the upper octaves. This is likely due to too much time spent resetting the saw waveform vs the short length of the cycle at high frequencies.