Showing posts with label Super saw. Show all posts
Showing posts with label Super saw. Show all posts

Tuesday, February 3, 2026

Confirming that the code is correct

This post shows the verification of the code in the previous post.


int24_t saw[7] = {0,0,0,0,0,0,0};

const int24_t detune_table[7] = {0, 128, -128, 408, -412, 704, -720};

int24_t next(int24_t pitch, int24_t mix, int24_t detune) {

int24_t sum = 0;

for (int i = 0; i < 7; i++) {
int24_t detunePitch = ((int48_t) pitch * detune) >> 23;
int24_t voice_detune = ((int48_t) detune_table[i] * detunePitch) >> 7;
saw[i] += pitch + voice_detune;

if (i == 0) {
sum += ((int48_t) saw[i] * 25) >> 7;
} else {
sum += ((int48_t) saw[i] * (mix >> 16)) >> 7;
}
}
return high_pass(sum);
}


Pitch

Pitch arrives at program memory location 0x0424, after some calculations pitch is stored at iram[0x65]. The same value arrives unchanged at the first oscillator calculation at 0x0455.
 
Pitch code is correct.


Detune

Detune arrives at 0x043f. The same value minus one arrives at the first oscillator calculation at 0x0455. Detune is stored in mulcoeffs[0]. 

With detune = midi 127 and pitch = midi 97, we get 

pitch = 421800 

detune = 164352

(pitch * detune) >> 23 = 8237

Test:

Osc 4 detune 8237 * 408 / 128 = 26255.4, from debugger: 26255

Osc 5 detune  8237 * -412 / 128 = -26512.8, from debugger: -26513

Osc 6 detune: 8237 * 704 / 128 = 45303.5, from debugger: 45303

Osc 7 detune: , 8237 * -720 / 128 = 46333.13, from debugger: -46334

 

Detune code is correct.

 

Mix

Mix arrives at 0x043c. The same value minus one arrives at the oscillator mixing code at 0x47a. Mix is stored in mulcoeffs[1] 

With mix = midi 127:

mix = mulcoeffs[1] = 2183167 (input - 1) 

mix >> 16 =  2183167 >> 16 = 33

 

Results below are found by stepping through the debugger. Iram contains the raw values for each saws.

Step 1: 

Iram 11 = -7537654

Result  = -1943302

Control: 

 -7537654 * 33 / 128 = - 1943301.4 // OK!

 

Step 2: 

Iram 6 = -8000810

Result = -4006011

Control: 

-8000810 * 33 / 128 = -2062708.8 // contrib from this osc

-2062708.8 - 1943302 = -4006011.8 // OK!


Step 3:

Iram 9 = 92969

Result = -3982043

Control: 

92969 * 33 / 128 = 23968.57 // contrib from this osc

23968.57 - 4006011.8 = -3982043.23 // OK! 


Step 4:

Iram 5 = -4968160

Result = -4952387

Control:  

-4968160 *25 / 128 = -907343.75 // center oscillator

-3982043 -907343.75 = -4952386.75 // OK!

 

Step 5: 

Iram 7 = 6361263

Result = -3312374

Control:  

6361263 * 33 / 128 = 1640013.12 // contrib from this osc

1640013.12 - 4952386.75 = -3312373.6 // OK!

 

Step 6: 

Iram 0b = 3650948

Result = -2371114

Control: 

3650948 * 33 / 128 = 941260.03 // contrib from this osc

941260.03 - 3312374 = -2371113.96 // OK!


Step 7:

Iram 0f is 6940947

Result = -581652

Control: 

6940947 * 33 / 128 = 1789462.0 // contrib from this osc

1789462.0 - 2371114 = -581651.1 // OK!

 

Summing code is correct.

 

Checking the detune coefficients

The integer coefficients we found:

[0, 128, -128, 408, -412, 704, -720]

The decimal coefficients in the presentation, that match Adam Szabo's detected ones:

[0, 0.01953125, -0.01953125, 0.06225585, -0.0628662, 0.107421875, -0.10986328125] 

These are 10/65536 times the integer coefficients (or 10 * (integer coefficient) >> 16). This holds true for all of the coefficients.

 

The coefficients are correct. 

 

The super saw code

I've spent the last couple of weeks trying to get to the bottom of the super saw code example after it became clear that it cannot simply be implemented the way it is written. Among other things, it's using DSP multiply high, and perhaps there are some other simplifications in there?

In general, these things didn't make sense:

  • The detune table didn't match the fractional detune coefficients we know and love.
  • Pitch * detune didn't make sense as it would lead to detuning of more than twice the base frequency
  • Summing of the saw waves would make the sum accumulator overflow, turning the output into a single saw wave of higher frequency than the individual saws.
  • The summing doesn't follow the curves suggested by Adam Szabo 

After thorough studies of the emulator and running code, I've managed to reproduce an accurate version of the code that runs on the DSP, but one that can be used on a normal processor. It's very close to the suggested version, but with some crucial differences:

  • Coefficients 4 to 7 are half of what they were presented as
  • Pitch * detune is a multiply-high, which is common in DSPs
  • The individual saw waves, including the center wave, are attenuated (divided) before summing to prevent overflow.
  • The summing curves are indeed different from what is expected. Specifically, the center oscillator is never attenuated when the others are increased. The curves are likely the effect of normalization or similar later in the code


The modified code looks like this. It utilizes variable roll-over, so it is crucial to use 24bit integers. Also, allow multiplication results to be 48 bits before shifting right. 


int24_t saw[7] = {0,0,0,0,0,0,0};

const int24_t detune_table[7] = {0, 128, -128, 408, -412, 704, -720};

int24_t next(int24_t pitch, int24_t mix, int24_t detune) {

int24_t sum = 0;

for (int i = 0; i < 7; i++) {
int24_t detunePitch = ((int48_t) pitch * detune) >> 23;
int24_t voice_detune = ((int48_t) detune_table[i] * detunePitch) >> 7;
saw[i] += pitch + voice_detune;

if (i == 0) {
sum += ((int48_t) saw[i] * 25) >> 7;
} else {
sum += ((int48_t) saw[i] * (mix >> 16)) >> 7;
}
}
return high_pass(sum);
}


Explanation of the code

Generating saw waves 

Oscillators are calculated by summing the current value with a new increment and letting the variable overflow/wrap around.  

The center oscillator is simply the previous value + pitch, pitch is nothing more fancy than the increment needed to get the variable to overflow the correct number of times per second.

Pitch/detuning 

For all the other oscillators, a detune base is calculated. This is the number by which the coefficients are multiplied.

The multiplication is a multiply high, e.g. it multiplies the two numbers but keeps only the upper part: 


int24_t detuneBase = (pitch * (detune >> 16)) >> 7 
detuneBase += ((pitch >> 7) * ((detune >> 9) &0x7f)) >> 7

or

int24_t detuneBase = pitch * detune >> 23

 

Now, the oscillator increment values can be written as the following:

int24_t osc2Inc = pitch + detuneBase
int24_t osc3Inc = pitch + (detuneBase * -128) >> 7
int24_t osc4Inc = pitch + (detuneBase * 102) >> 5
int24_t osc5Inc = pitch + (detuneBase * -103) >> 5
int24_t osc6Inc = pitch + (detuneBase * 44) >> 3
int24_t osc7Inc = pitch + (detuneBase * -45) >> 3

 

And if we make all of them shiftable by >> 7:

int24_t osc2Inc = pitch + (detuneBase * 128) >> 7
int24_t osc3Inc = pitch + (detuneBase * -128) >> 7
int24_t osc4Inc = pitch + (detuneBase * 408) >> 7
int24_t osc5Inc = pitch + (detuneBase * -412) >> 7
int24_t osc6Inc = pitch + (detuneBase * 704) >> 7
int24_t osc7Inc = pitch + (detuneBase * -720) >> 7

 

From this, we get the correct coefficients: 

{0, 128, -128, 408, -412, 704, -720}

 

And the general formula:

saw[i] += pitch + (detuneBase * coefficient) >> 7


Summing, multiplication by mix
 
Mixing is very simple:
 
Osc 1: (saw[0] * 25) >> 7 // divide by 0.1953 to prevent overflow
Osc n: (saw[n] * (mix >> 16)) >> 7 // uses 8 MSB from mix.

 

Inputs

The code above, while understandable, is quite unusable without the proper input values. Let's have a quick look at what they mean. I've added a bit about where they can be found in the DSP code in a different post.

 

Pitch

Pitch input, without any modulation, ranges from 1555 for midi note 0 to 1338944 for midi note 117, which is the last note that has a unique value (e.g. the highest playable note).

Pitch is simply the number that must be added to the 24bit accumulator every step to make it overflow f times per second. 

For example: Triggering note 97 sets pitch to 421800. For 421800 => 16777216 / 421800 = 39.7753 steps are needed to get the variable to roll over. At 88.2kHz that means f = 88200 / 39.7753 = 2217.46Hz. 

Looking at the midi table, that's the exact frequency represented by midi note 97.

 

Mix

Mix ranges from 102400 to 2183168, and follows a straight line. 128 discrete steps are available.

Ex: Midi value 127 arrives as 2183168 (MSB-aligned, sign + 14bit precision 24bit int). This is the value used throughout the code. As the value is transmitted as two 8bit coefficients, it can also be thought of as 4264 internally in the MCU*

* the value is transmitted as 8MSB, which includes the sign bit, and then 7bits (sign bit not used), joined into a 15 bit number and 0 padded to a 24bit signed int.

An important ting to note is that the mix control signal is completely linear, and it only affects the detuned oscillators, not the center one. For those who have seen the Adam Szabo paper, he states that the outer oscillators follow a curved response and that the center oscillator is attenuated as the others are turned up. 
 
This effect is absolutely real - but it does not stem from the supersaw generation code. In fact, the output from the DSP that creates the supersaw shows the output one would get from the code above. However, once the signal reaches the DAC, at the output of DSP 4, the signal does indeed function as Szabo measured. Somewhere along the line, the amount of each frequency is changed, perhaps in some form of total-energy or normalization process.
 

Detune 

Detune ranges from 512 to 164352 and follows a exponential-ish curve (more on the details later). 128 discrete steps are available.

Ex: Midi value 127 arrives as 164352 (MSB-aligned, sign + 14 bit precision 24bit int). This is the value used throughout the code. As the value is transmitted as two 8bit coefficients, it can also be thought of as 321 interally in the MCU.

 

Smoothing 

While not shown in the code above, mix and detune are smoothed, e.g. changes are not immediate. Instead they are changed gradually during a few steps after setting. This happens in the DSP-code, not the MCU.


Input values

Here is how to get the correct input values for pitch, mix and detune


Pitch

The formula for pitch, given midi note n is
 
frequency f = 400 * 2^((n - 69) / 12)
pitch = round(f * 2^24 / 88200)

Now, this won't give the exact values the JP8000 uses as those are a bit imprecise, but the difference is very small.
 

Mix

The mix control signal is linear and follows these rules:
 
0 = 102400
1 to 127: += 16384 
 
When it arrives at the oscillator mixing code, only the upper 16 bits of mix are used, which means the control curve internally is
 
0, 1 = 1
2 to 127: increase by 1 for every four steps. 

 

Detune 

As mentioned, detune follows a sort of exponential curve. In reality, it's made up of linear segments. The value is transmitted from the MCU to the DSPs as two 8bit numbers that together make up a 15 bit (sign bit + 14 databit) numbers, so it can be thought of as a 15 bit number inside the MCU.
 
If doing so, the curve follows these rules:
 
0 = 1
  0 to  63: increment by 1 every second step
 64 to  80: increment by 1 every step
 81 to 120: increment by 2 every step
121 to 123: increment by 8 every step
       124: increment by 16 
       125: increment by 32
       126: increment by 96
 
To get the value as seen by the DSP, multiply by 512.
 
There is a special case with 103, it is loaded as 40448. I'm not sure why, it looks like a bug. However, it IS present at the point where pitch * detune is calculated in the emulator, and it does affect the calculation.
 
X: Midi values, Y: Detune values (15bit)



Friday, January 16, 2026

Mix and oscillator amplitudes, more research

I just can't let this one go. The code appears to add a linear amount of the side oscillators to the sum, but Adam Szabo says differently - the center oscillator is attenuated and the outer ones follow a curved gain.

I just had a happy accident. I am trying to find the contribution of each oscillator to the total if one normalize the sum - saying the total should always be 1.

However, I only added one single outer oscillator - but the output was very interesting:

Here it is compared to the graph in the article:

 

Here, the value of oscillator 1 is 1 / (1 + mix), whereas the plot for the others is mix / (1 + mix). The plots are quite similar! It really makes me want to understand this even more!

But - if I assume that ALL 6 outer oscillators should be included in the normalization, everything breaks down, so clearly that's not correct.

Going back to the article, we have this graph (Figure 9):

Note that the amplitudes of 5, 6 and 7 are higher than 1, 2, 3.

In the text, Szabo says that 2, 3, 5, 6 and 7 are removed, leaving 1 and 4, as illustrated in the first graph.

I don't know if he DID measure those too, but there is a chance that they don't follow the exact same curve. We'll see if we can figure that one out.

Also, let's renumber the spikes in the plot to match the order in the detune_table:

[0, 318, -318, 1020, -1029, 1760, -1800], let's call them A-G to keep them separated

That gives us:

A = 4, B = 5, C = 3, D = 6, E = 2, F = 7, G = 1

In case it matters, what is called 1 here is actually the last element added in the summing in the ESP code. 

 

Now, I'm not entirely sure how to interpret Figure 9 in terms of "max wave amplitude". The spectrum has peaks of a certain width, not just a single frequency, and there are no units on the Y axis. If the scale is linear and one assumes that the amplitude of each wave in the result is actually propotional to the max value of the center oscillator, we get the numbers in the table (each pure frequency is a sine wave, so the highest peak would correspond to the root frequency of the saw waves, wouldn't it)?

It looks like that's what Szabo means that they are, so let's accept that.

If so, the sum of all amplitudes is definitely not 1. Could the perceived total "loudness" be equal if one uses dB instead of a linear scale? The total energy or something?  

And in any case, how does one go from the sum += saw[i] * mix to this thing? 

My own measurements

I wanted to confirm my understanding of Szabo's graph, so I fired up JE-8086 and coded a spectrum analyzer in web audio. I fed the audio from JE-8086 back to the input of my mac using the virtual microphone/input "VB-cable".

I used a linear Y-axis and a logarithmic X-axis. Then, with detuning at max, for every line on the mix pot (11 in total) I screenshot'ed the spectrum. Finally, I went through every graph, measuring the height in pixels.

Here is the first and last spectrum plots:



The measurements were of course wildly inaccurate, and the spacing between the mix values not quite even as I couldn't see the slider value in the display (and also, the slider resolution wasn't high enough). 

Here are the values:



And, more importantly, the plot of the values relative to max value of the center oscillator:


 



This is indeed very cool. It does confirm most of what Szabo described - the center oscillator is fairly linear and the others are definitely curved, and the center oscillator ends at an amplitude lower than the outer ones. There are some small differences though:

- I don't get the feeling that the outer oscillators actually go DOWN in amplitude at the end. The 7th oscillator appears to go slightly  down again, but I think it's more likely a measuring error. 

- The higher pitch / right hand oscillators have a higher amplitude than the lower ones. This matches what can be seen in Figure 9 in Szabo. The top oscillator is even higher though, and that does not match. I am still not sure if this is an artifact of the spectrum analyzer or if it is real. It could be an artifact of approximate multiplication or running average or something. 

- Something else to note: Both in mine and Szabo's spectrum analyzers, the minimum amplitudes for the outer oscillators is not zero. If the summing is actually saw[i] * spread, there should be no trace of the oscillator if spread is 0. Very strange. Also - why do they call it spread and not mix?

Tuesday, January 13, 2026

A closer look at the super saw code

In this post I try to understand most of the super saw code found by the Usual Suspects when reverse engineering the Roland/Toshiba TC170C140 ESP chip. Since I have no real DSP programming experience, it took some time to realize what is actually going on.
 
Presumably, this is the code for the ESP emulator itself: https://github.com/dsp56300/gearmulator/tree/main/source/ronaldo/esp 


Coefficients - floats 

The coefficients listed in the usual suspects' presentation, when represented as floats, are:

[0, 0.01953125, -0.01953125, 0.06225585, -0.0628662, 0.107421875, -0.10986328125]

if we multiply by 8192 we get

[0, 160,  -160, 509.9999, -514.99999, 880, -900]

and if we presume that the strings of nines are the result of a division somewhere, we get

[0, 160,  -160, 510, -515, 880, -900]

Pretty neat!

8192 is 2 to the power of 13. The real values may be that, or perhaps either 14 or 16 bits, so either  

[0, 320,  -320, 1020, -1030, 1760, -1800] 

or

[0, 1280, -1280, 4080, -4120, 7040, -7200] 

 

Coefficients - integers

The code example lists these integer coefficients

[0, 318, -318, 1020, -1029, 1760, -1800]

Now that's  really cool. Those are almost a prefect match for the 14 bit representation of the coefficients. 

There are some strange mismatches though - 318 and -1029 instead of -320 and 1030. 

I'm not sure WHY this is yet.

Chat GPT suggest:

The small mismatches (318 vs 320 etc) come from:

  • rounding
  • deliberate asymmetry to reduce beating regularity
  • truncation after multiplication
  • and accumulator overflow behavior

But the mismatches are there in the originals, not the calculated values, and if they had the originals, they would have used a shared factor of 16384 when dividing down to the floats (weirdly, even 1020 / 16384 is listed as 0.06225585, when the real value - without any rounding error - is 0.06225586).


Pitch value

The pitch value is how much we need to increase the saw wave for every sample. We now know that the JP8000 uses a sample rate of 88.2kHz, and 24bit accumulators that have a range of 16,777,216 (bipolar)

The exact pitch range of the JP8000 is not known, but let's for a start consider the midi standard.

Midi note 0 (C-1) has a frequency of approximately 8.18 Hz, while the highest note, MIDI 127 (G9), is around 12,544 Hz 

Running at 88200Hz, every cycle of

8.18Hz is 10782.396 samples long

12544Hz is 7.03125 samples long.

With a 24bit accumulator, each increment will be

for 8.18Hz:  1555.98, e.g. 1556

for 12554Hz: 2386092,942, e.g. 2386093.

 

From this, we can assume that, approximated

Pitch range is1556 to 2,386,093 

Knowing this, the only unknown in the detuning equation, is the int24_t detune parameter.

 

Detuning

Given the center oscillator frequency F0,

When using floats, the outer oscillators should have a frequency Fn:

Fn = F0 * (1 + floatCoefficient[n])

When using the integer coefficients, we instead get

Fn = F0 * (1 + integerCoefficient[n] / 2^14)

Or 

Fn = F0 * (1 + integerCoefficient[n] >> 14)

Now, doing the bitshift on the coefficient alone would lead to a massive loss of precision (or rather, all the coefficients would become 0), so at very least, we have to do the bitshift after multiplying with F0:

Fn = F0 + (F0 * integerCoefficient[n]) >> 14

Ah, this is starting to look like the code, exciting!  

 

Detune amount

There is a third number in the detune calculation. The code calls it "detune" but in reality it's detune amount. It says how much of the detune coefficient to apply. 

According to Szabo, pitch * coefficient is the _maximum_ amount to apply. That means the detune amount should be a ratio between 0 and 1. This, of course, is not possible using an integer, without a division following the multiplication. 

Let's take a look at the original equation:

int24_t voice_detune = (detune_table[i] * (pitch * detune)) >> 7 

We know that detune somehow should give us a scaling between 0 and 1, so let's ignore how we get there for a second and remove it. That leaves us with detune_table[i] * pitch. 

We also know that we should divide this result with 2^14, though that is not included in the equation. Let's include it still, it has to be there somehow.

A quick check of the multiplication and following bitshift in the detune frequency calculation:

The lowest possible value is (1550 * 318) >> 14, which is 30. (lowest frequency, lowest detune without detune amount scaling).

The highest possible value is  (2386093 * 1800) >> 14, which is 262144. (highest frequency, highest detune amount).

The results are within a 24bit int range. However, the intermediate value from the multiplication is 4,294,967,400, which is much higher than what can be stored in a 24 bit int. Under normal conditions, this would make everything overflow. To be able to properly store the multiplication result, we need a division, and one that happens before the result is stored. Something strange is clearly going on.

Side note: The max result is even slighty higher than what can be stored in an uint32 (4,294,967,295). At the same time, its so amazingly close that it's hard to believe that its just random? And actually, if we go back to the highest frequency, it's actually 2386092,942. With that fractional result, the product is 4,294,967,295.6 - a mere 0.6 above. This is too weird to be a coincidence? Also, it turns out that the max frequency isn't 12554, it's slightly lower. That would keep the detune within a uint32 range. Interesting.


Now, lets go back to the equation and reintroduce detune:

int24_t voice_detune = (detune_table[i] * (pitch * detune)) >> 7

pitch * detune, let's call it pitchWithDetuneAmount will be calculated first. Max pitch is 2386093, so detune may be up to 3 without overflowing. That doesn't make much sense.

Next,  detune_table[i] * pitchWithDetuneAmount is calculated. detune_table[i] is at most 1800, so it will overflow if pitchWithDetuneAmount is larger than 4660.

Finally, everything is divided by 128 

And all of this, with detune at max, should not be larger than 0.10986328125 * pitch.

In a normal situation, we should divide detune table by 2^14, and detune by a maxDetune to make it into a ratio between 0 and 1. It is highly probable that maxDetune would be a factor of 2 as well, to make division a bitshift here too.  

Side note: maxDetune needs to be high enough to represent the curve shown by Szabo, at least if there are 128 different values not linearly spaced apart.

But the mystery remains. Where have the remaining divisions gone? We see some division (>>7 is the same as / 128) but that's not enough and it's not in the right place to prevent overflow.

 

DSP magic

I admit it, I had to ask ChatGPT about this one. At first it was reluctant to admit that there are something that makes division/bitshifts superfluous, but then everything dropped into place!

Enter multiply-high

DSPs have two major tasks - summing and scaling. Summing is +, and scaling is multiplication followed by a division. 

In fact, scaling is so important that most DSPs do multiplications in a slightly different way. They multiply the two numbers into an accumulator with twice the bit count of the factors, but then it only returns _the high order_ bits. E.g. if it multiplies, say, two unsigned uint16_t variables a and b, it would store the intermediate result in a u32_t, but then only return the 16 MSB. This is equal to bit shifting >> 16 or dividing by 65536. If we let a be our signal value, and b a scaling factor, this turns b into a scaling between 0 and 1!

So there you go, we get free bit shifts, invisible in the code.

In other words, the code the Usual Suspects is showing is is not normal C code, it's DSP code (of course...) meaning the * does not do what it normally does, it also bit shifts.

 

Implicit bit shifts 

Lets go back to the code again and see how this works out

int24_t voice_detune = (detune_table[i] * (pitch * detune)) >> 7

 

First we do 

pitch * detune

which we called pitchWithDetuneAmount earlier.

By making detune max equal to the bitshift included in *, it becomes a ratio between 0 and 1. Hooray! 

Then we do 

detune_table[i] * pitchWithDetuneAmount

Again, * will introduce bitshifting.


If we go back to the paragraph about multiply-high, an int24_t * int24_t would result in a 48bit intermediate, of which the upper 24 are returned (it may be slight differences working with signed ints but the principle is the same).

Shifting >> 24 is fine for detune amount. It would mean we could use the full 24 bits as a factor, getting any detune amount curve we could possibly want.

But shifting detune_table * detuneAmount by 24 is too much. It should only be 14, and even then 7 of the shifts are done outside the parenthesis.

Now, it's not important exactly how the bit shifts are done to understand the code. We can just accept that the code

int24_t voice_detune = (detune_table[i] * (pitch * detune)) >> 7

is equal to

pitch * (detune / maxDetune)* detune_table[i] / 2^14

without overflows etc. It is only important if we want to run the exact code and use the same range for detune amount.


Different bit shift?

There is however a posibility that the DSP doesn't actually shift by 24. Maybe it shifts by 7? That would make  

(detune_table[i] * pitchWithDetuneAmount ) >> 7

the same as

detune_table[i] * pitchWithDetuneAmount >> 14 

in a normal system.

It would mean that detune amount has to be 7 bit if it uses the same multiplication-high, leaving 128 steps of detune amount. That does not work well with the Szabo curve, but it is possible. Or maybe the C code is just inaccurately translated from assembly and that they use different multiplication operations.


The DSP (ESP) emulator code for the JE-8086 is on github, so we can peak at what it actually does. I have not studied it in detail, but there are traces of a configurable bit shift multiplication.

Studying https://github.com/dsp56300/gearmulator/blob/main/source/ronaldo/esp/esp.hpp may shine some light on this.

multResult in esp.hpp shifts the result after multiplication, 5, 6 or 7 places. It uses two bits of the instruction to select the shift, and 0,0 will default to 7 bit shifts. That sounds exactly like what we are looking for.

So, while not proving that 7 is the correct answer here, at least it makes it plausible that the ESP does infact use a different shift than 24. 

UPDATE: The multiplication (kMAC in the code) does two bitshifts. First, it shifts the second term by >> 16, meaning it only uses the 8MSB. THEN it does an up-to 7 bit right shift). In other words, detune and spread are not 0-128, they are the full 24bit range but only the 8MSB are used. There is also a double precision multiplication available so it is possible that is used for higher resolution

Something that may support this theory is that on a slide about the ESP, it says that it has a 24 x 8 bit multiplier. This seems to indicate that it does NOT have a 24 x 24 bit multiplier, and that, combined with signed arithmetic where one bit of the 8 bit variable is used as sign, would make a shift of >> 7 quite plausible and 7 bit (positive value) detune the way to go. 

It does however leave a question as to how the multiplication of detune_table and pitch works since both are definitely > 128. Perhaps it does multiple 24 x 8 multiplications?

--> It looks like it is possible to do that. The final bitshift will always be >> 7 which makes the last >> 7 explainable. This would make the detune_table[i] * pitch >> 14 multiplication possible. Detune and spread would still have to be max 127.

Example: 24 × 24 multiply-high using three 24×8 blocks

According to ChatGPT. Test this!

Let the 24-bit multiplier be split into bytes:

B = b2·2^16 + b1·2^8 + b0

You compute:

P0 = (A·b0) >> 7 P1 = (A·b1) >> 7 P2 = (A·b2) >> 7

Then re-align:

Result = P0 + P1 << 8 + P2 << 16

Substituting:

= A·(b0 + b1·2^8 + b2·2^16) >> 7 = (A·B) >> 7 (approximately)

Update: Double precision multiplication

The ESP supports "double precision" multiplication. I have not yet fully understood the result but essentially it does exactly what is suggested above - it first multiplies A with the 8MSB of B. It then multiplies A >> 7 with (B >> 9) & 0x7F, or bits 15 to 8 (0 indexed) of B

first time: acc += ((mulInputA_24 * (mulInputB_24 >> 16)) >> shift)        

second time: acc += (((mulInputA_24 >> 7) * ((mulInputB_24 >> 9) & 0x7f)) >> shift)

... I am missing something here, TBC. 

Spread 

There is a third multiplication in the code, saw[i] * spread. Just as with detune, this looked very strange and would lead to an overflow in a normal system. But with multiply-high this too becomes a scaling from 0 to 1, just as we needed. It could, as detune, be a value between 0 and 127 and work fine with >> 7. Again, it's not important to the understanding of the code, we can just accept that it's a scaling factor.

 

Conclusion

There are some questions left unanswered. Shifting by 7 on * means that detune_table[i] * pitch may still overflow (that 32bit thing above, remember), and the curves of detune and spread can't be explained properly. And finally, as mentioned in the previous post, summing of the seven saws will overflow.

In general, however, the code looks like it could do exactly what we think. If we were to reimplement it we would just take care of these issues - increasing sum to 32bit and doing the appropriate bits shifts manually, and selecting whatever resolution for detune and spread that we want. 

The only thing I cannot explain at the moment is how Szabo could see a attenuation of the center oscillator when doing mix (spread), as that is not part of the code. Perhaps it is some kind of normalization effect, that the center oscillator contributes less to the total. Guess that one just has to remain a mystery for the time being. 

Thursday, January 8, 2026

The super saw code from the Usual Suspects

At the 39C3 conference, the Usual Suspects talked about how they reverse engineered the Toshiba DSP chip from the JP80x0. In itself an incredible feat, and a super exciting talk, but the one thing that REALLY caught my interest, was what they claim to be the code for the original super saw.

https://www.youtube.com/watch?v=XM_q5T7wTpQ&t=1804s 

They describe it as simply 7 saw waves, high pass filtered, with detuning, running at 88.2kHz to prevent aliasing within the audible range (?).

The code even shows the detuning coefficients, and state that it's integer maths, making them a bit hard to get right.

 

Now, of course I had to see if I understand the code. Here is a screenshot:

 

Not much. I assume next is run once per DAC update, i.e. 88200 times per second.

The saw oscillators are simply 24bit signed integers used as accumulators. For every round, "pitch" is added to the accumulator. Once the value reaches the max value that can be stored in a 24bit int, it overflows and wraps to negative minus. This way, by continously adding to the accumulator, we end up with a saw wave. 

Oh, btw - looking at the code, the initialization of the array is a bit strange. This being a global array, it should automatically be initialized to all 0s. {0} explicitly sets the first element to 0, why is that needed?

Let's for a start ignore detuning. If we set detune to 0, the whole voice_detune parameter goes away and saw[i] is just incremented by pitch for every cycle.

Also, let's set spread to 1, so all oscillators have the same amplitude.

 

Assuming the saw waves are in perfect phase, summing them would give us a saw wave that increases 7 times faster than a single wave. But then there is something weird. 

sum is also defined as a int24. My only way of understanding this is that it will overflow too, just like the saw wave accumulators. And that, would lead to a saw wave with the same amplitude as the individual waves, but with a frequency seven times higher!

Lets reduce the number of oscillators to 2 and introduce a phase shift of 25%. Without overflowing, this would lead to some tops higher than max, some lower than min and some cycles where the amplitude is less than min and max. But with overflowing, the parts above and below max/min fills in the gaps, and once again we're back to having a single waveform with a 2x frequency but the same amplitude:

Red horizontal lines are where the sum accumulator overflows.

 Now, the PHASE of the output wave is different from the initial wave. 

Here is a way of thinking about this. 

For every step. each saw wave contributes "pitch" to the sum. The saw waves wrap, but the rest of pitch will be added to the bottom. This is similar to having a single saw wave with 2*pitch increase for every step.

Now consider different pitch values for the two saw waves (=different frequencies). Each wave still contributes its pitch to the sum, creating a single saw wave with pitch equal to the sum of the two other saw waves.

This extends to the rest of the saw waves, adding another saw wave just adds its pitch to the sum. In the end, the seven waves end up as a single wave with its pitch being the sum of all the pitches. 

Here is an example. The grey line is all saws, with slight detuning, summed up without overflow (and plotted in a chart where y is at most 8 times that of a single saw wave. The blue line is the same waves summed with overflowing.

The horizontal lines divide the range into 8 parts, each corresponding to "one overflow".

 

 

If you look carefully, you can see that at every discontinuity, the part protruding above a grey line, is exactly the same as the part missing from the bottom and down to the previous grey line. When using overflow (or modulo), the top will wrap and be added to the bottom. Any of the divides that are empty, simply goes away in the wrapping, and we end up with the blue line.

 

Ok, that was a convoluted way of saying -  I don't understand how the sum code is supposed to work. Saw waves of any frequency will always combine to a single saw wave of higher frequency if the sum also overflows. As neither the frequency nor the detune of a wave changes, the sum wave will stay unchanged.

If sum was a 32bit int, this would work fine and we would get an ever changing combination of the waves. 

Detune and detune coefficients

Now, as for the other parts of the code, they have me confused as well, but maybe they use overflow as part of a trick? 

In other parts of the presentation, a comparison between Adam Szabo's coefficients and the "real" ones is done. The coefficients are fractions, small ones too. To calculate a detune frequency, one uses 

basefrequency * (1 + coefficient)

or 

basefrequency + basefrequency * coefficient.

In the code above, 

saw[i] = pitch + voice_detune

or 

saw[i] = pitch + ( detune_table[i] * ( pitch *  detune )) >> 7

Now, I presume the parenthesis are place the way they are for a reason, perhaps the parts inside the parenthesis overflow in a certain way that makes things work out, but substituting /127 for >> 7 and reordering gives us

saw[i] = pitch * (1 + detune_table[i] * detune / 128) 

The lowest coefficients are 128, and the lowers integer value for detune is 1. Following that, we end up with 

saw[i] = pitch * 2

This is clearly wrong. Perhaps the overflow inside the parenthesis, and the values chosen for detune, will lead to something that, when divided by 7, is always much less than (and propotional to) pitch?

As for the coefficients themselves, the individual propotions are not the same as for the fractional coefficients, so something strange is going on there as well.


Spread

Finally, we have "spread"

The outer saw waves are multiplied by spread before adding them to the sum. Presumably, this is the same as "mix" on the JP8000. 

But again, being integers, spread can only INCREASE the amplitude of the saw wave (or perhaps rather the pitch, since the product of saw[i] * spread will overflow. 

In Adam Szabo's paper, the center oscillator amount is reduced linearly, while the outer oscillators are increased by a curve:
 

Perhaps there is some kind of normalization going on, where, by increasing the outer oscillators, the relative contribution from the center one is decreased? 

Again, I'm confused. 

I have a feeling that at least one trick is used here. Since division is probably extremely expensive on a DSP which is built for Multiply and Accumulate, perhaps one instead uses multiply + overflow? (bitshift >> 7 is used to divide by 128, but this only works for powers of two).

 

I really wish someone could confirm a couple of things.

First of all, is the code completely correct - while I don't understand it at the moment, at least that would give me more confidence in looking for the solution

and

confirmation that the output of this code is indeed "samples" that, after filtering, will be output to a DAC (or the next DSP in the case of the JP8000).


My analog super saw

Years ago I build an analog 7 saw oscillator with control circuitry that emulated the control curves seen in Adam Szabo's paper. I had the curve for the detune pot using a three leg approximation, and something that looked close to the mix curves. 

I actually built the whole thing before I realised

1) Mixing the saw waves would lead to clipping if the headroom was not high enough and

2) Part of what makes the supersaw sound the way it does, is that it's digital (D'oh).

 

Adam wrote a few things too, in the paper or on a forum, I can't quite remember. Quoted from memory: - The naive approach of generating multiple saw waves would not work as the JP8000 was not powerful enough

- He had discovered some kind of trick that Roland would not tell the world about.

Not sure if that was all smoke and mirrors, but I was hoping that this last trick was somehow related to how to prevent the overflow while still keeping gain high.

 

Oh well. Time to go to bed.  

Wednesday, July 14, 2021

Super saw analysis ideas

I found a post on Matrixsynth today that lead me to a second paper analysing the JP8000 super saw. 

The post: https://www.matrixsynth.com/2021/07/korg-nts-1-nutekt-digital-synthesizer.html

The project: https://github.com/GrahamJamesKeane/UberSaw

The project page: https://korgnts1beginnersguide.wordpress.com/

Adam Szabo: How to Emulate the Super Saw: https://korgnts1beginnersguide.files.wordpress.com/2021/07/szabo_adam_10131.pdf

Alex Shore - An Analysis of Roland’s Super Saw Oscillator and its Relation to Pads within Trance Music: https://korgnts1beginnersguide.files.wordpress.com/2021/07/ananalysisofrolandssupersawoscillatoranditsrelationtopadswithintrancemusic-researchproject-a.shore_.pdf


This got me thinking - I still think the detune response looks like it consists of three linear components instead of a 11th order polynom like Adam Szabo shows in his paper (I did in 2014 as well when making my analog 7-saw module).

I really want to test my theory in detail, but that requires some automation. Here's my idea:

- Control the JP through webMidi

- Loop through all midi values for detune

- Use a js spectrum analyzer, for example https://github.com/hvianna/audioMotion-analyzer to find the frequency components. We should be able to find 7 distinct maximums (the 7 oscillators). 

- For each maximum, record the pitch and amplitude, possibly as an average over several samples

- Build tables for each oscillator/detune setting and calculate spread and pot response to see what the actual response is.


Edit: I've been looking at the Web Audio API lately, and using it directly could be a good option. It works by chaining nodes, one of them being a FFT node. I assume that can be used for finding the maximums. 

Saturday, July 30, 2016

The Ultrasaw explained

While looking at the Matrixbrute from Arturia I noticed that it has an additional waveform called the Ultrasaw. I have heard about it previously, but got curious about exactly what it was and started looking at the documentation.

Apparently it is two additional saw waves with the same frequency as the first one, but phase shifted. The phase shift varies and is controlled by two built in LFOs. One wave has a constant phase shift rate while the other one is user variable.

I began thinking about how I would create such a circuit. Then I discovered that Yves Usson, designer of the xxxBrutes, had actually posted the MiniBrute circuit diagram on his site Hack a Brute.

I have studied the circuit closely and it seems half of the design matches my initial thoughts, but how the wave is phase shifted is really quite simple and ingenious. It resembles how my saw sub oscillator works, by shifting parts of the original wave up and down.


Let me explain.



The circuit looks like this (disclaimer: I do not own this circuit diagram, nor have I asked for the permission of Arturia or Mr. Usson to post it here. It is not in the public domain and is only reprinted here for educational use).

Figure 1

I have separated the circuit into three logical parts:
- The LFOs controlling the rate of phase change (left)
- The phase shifter (center)
- The wave mixer and volume control (right)

The LFOs

The LFOs consists of two main parts. To the left is a triangle wave oscillator. According to the MiniBrute documentation, the top one is fixed at 1Hz while the bottom one is user variable between 0.1Hz and 10Hz. The triangle wave design is the exact same one as the one in Texas Instruments' "Op amps for everyone", see Circuit 1-9 here or A.5.8 on page 446 of the original book. (PS: The book uses a unipolar power supply while the MiniBrute has a bipolar (+/- 12V) PSU).

To the right of the triangle wave oscillators is a triangle-to-sine wave converter. Its design is the same as the one in mr Usson's Yusynth VCO module.

Formula A-49 in the TI book gives us the amplitude of the triangle wave oscillator: Amplitude = +/- 24V * 47kOhm / 2 * 120kOhm = +/- 4.7V.

I have not calculated the output amplitude of the sine converter. Neither do I know the amplitude of the input saw wave, but it would be a reasonable guess that it is +/- 5V, a common value in synths.

A sine wave amplitude of +/- 5V would give a +/-100% phase shift in the phase shift in the next stage. Since a phase shift of -10% is really the same as a +90% percent shift, it seems unnecessary to have a shift larger than +/- 50%. It could even be that a smaller range is desireable, who knows. In the next section I'll assume that both the phase shift CV (sine LFO output) and the saw wave amplitudes are +/- 5V. It makes no difference for the understanding of the circuit.

The phase shifters

This is the funny part. The phase shifter circuits take a control voltage (the output from the LFOs) and the output from the MiniBrute's saw wave oscillator, and shifts the saw wave phase back or forward.

But how?

For simplicity, lets forget that the CV is coming from an LFO and instead treat it like a constant value. The control voltage (CV) desides how much we want to shift the saw wave back or forth. It does this by comparing the CV with the saw wave. When the rising input saw wave reaches the same value as the CV, thats where the shifted wave will start its cycle. This means that the phase shift is relative to the frequency of the input wave, it stays the same (in percent or degrees) even if the input saw wave's frequency changes. (This, by the way, was what I got right in my initial idea).

When the circuit has found where it wants to reset the saw wave, it sort of "chops off" the top of the input wave and moves it to the bottom of the wave instead. It then re-centers the wave for an even amplitude, and voilĂ , the saw wave has been shifted.

The detection and shifting happens in three sub circuits that are then summed to get the shifted wave:
1) The original saw wave
2) A comparator that compares the saw wave and the CV
3) An inverter that inverts the CV

Number 1 and 3 are simple to understand, but number 2 requires a bit of explanation.

An op amp without any feedback acts as a comparator. Whenever the input on the positive terminal is higher than the input on the negative terminal (usually called V ref), the output will instantly change to equal the positive supply voltage. If the input on the positive terminal is below the input on the negative terminal, the output will change to equal the negative supply voltage.

In our circuit the saw wave is connected to the negative terminal and acts as Vref:

Figure 2

Here is a graph that shows the how the input saw wave and the CV relate to the output of the comparator:
Figure 3

Again, whenever the saw wave is lower than than the CV, the output is 12V. When the saw wave is higher than the CV, the comparator output drops to -12V.

The output is scaled by the 220kOhm resistor and the diode at its output. PS: I have not thought enough about what the diode actually does. It may chop of the negative output from the comparator. For now we'll assume that the output is bipolar and scaled to +/- 5V.

Now, this is where the magic happens. The output from the comparator is summed with the original saw wave and the inverted CV, and out pops a shifted wave. Lets see how.

First, lets see what happens when we sum the saw wave with the comparator output
Figure 4

To make things easier to understand, I've color coded the parts where the CV is higher than the wave red and the parts where the CV is lower than the wave blue. As mentioned earlier, the point where the color changes from red to blue is the point where we want the phase of the output wave to start.

Summing the saw wave and the comparator output effectively moved the blue parts down and the red parts up, in a way that makes the lines align again. the fourth graph in the figure above shows how the result (red) is phase shifted in relation to the initial saw wave (grey dotted lines). It is however also shifted vertically.

So how do we fix this? Take a closer look at the first graph in figure 4. The top corner of the red triangles are what becomes the top corners of the resulting wave. The top corner should be at exactly 5V in the output. The comparator output will move the tip upwards 5V, but since the corner starts at a positive voltage instead of 0V it will end up higher than 5V. Thus, we have to move it back down a bit. How much? Well, things had been ok if the corner was initially at 0V, But it is at CV volts (remember, that is the definition of the corner, the intersection between the CV and the saw wave). Thus, we have to move everything downwards by CV volts:


The last graph in figure 5 shows the result - the saw wave has been phase shifted. The two operations in figure 4 and 5 plus the scaling of the comparator output happens simultaneously in the summer of the Phase Shift blocks of the circuit.

Finally the two saw waves are summed and sent through the SuperSaw (!) amount pot RP1A, which acts as a voltage divider scaling the output to between 0 and 100%. The SawAnimator out is then mixed with the raw saw wave elsewhere.