You are on page 1of 31

AA of Oscillators and Distortions with Pre Integrated WaveTables T.

Rochebois

Anti Aliasing Oscillators and


Distortions with Pre Integrated
Wave Tables
rev 0.4 14 september 2016
by T.Rochebois*

rev0.1: (2 sept 2016) initial release


rev0.2: (3 sept 2016) added example code : Anti Aliased 3 Operator Parabola Phase Modulation
Synth
rev0.3: (12 sept 2016) added example code: Walsh synth.
Rev0.4:(14 sept 2016) added example code:Tubey distortion.

1 Notation
v() function
v(1.3) function evaluation
v[] table
v[7] access to a table (integers only)

Most short code examples are sort of jsfx (plugin language embedded in the Reaper DAW).

* I'm DR from the Universit Paris Sud Orsay (French Thesis about Multiple Wavetable Synthesis). I worked with the
Canam Computers company in the late 90s on many audio DSP algorithms including the Ongaku pitch tracker, the
EdgE anti aliasing method (very similar to "bleps") and variants of DPW. Contact:
Smashed.Transistors@gmail.com

1
AA of Oscillators and Distortions with Pre Integrated WaveTables T.Rochebois

Contents
1 Notation.............................................................................................................................................1
2 A simple Wavetable Oscillator...........................................................................................................3
2.1 Wave tables, How to read them.................................................................................................3
2.2 Digital Oscillators, Phases and Frequencies..............................................................................4
2.3 Digital Oscillators, Accessing the wavetable.............................................................................4
2.4 Digital Hell: Wavetable Aliasing...............................................................................................5
3 The popular cure: MIP MAPs............................................................................................................6
4 The old trick: Pre integration.............................................................................................................6
4.1 Memories...................................................................................................................................6
4.2 Back to the problem...................................................................................................................7
4.3 Oversampling.............................................................................................................................7
4.4 Area............................................................................................................................................7
4.5 Integral.......................................................................................................................................8
4.6 Pre integration............................................................................................................................8
4.7 A perfect world ?........................................................................................................................8
5 Cousins..............................................................................................................................................9
5.1 Differentiated Parabolic Waveforms..........................................................................................9
5.2 Differentiated Polynomial Waveforms......................................................................................9
6 Back to Pre integrated wavetables...................................................................................................10
6.1 Higher order pre integrated wavetables...................................................................................10
6.2 Anti Aliased Distortion............................................................................................................10
6.3 Anti Aliased Phase Modulation................................................................................................11
Appendix A Commented JSFX code examples..................................................................................12
Appendix A.1 An Anti Aliased Walsh Function synth...................................................................12
Appendix A.2 An Anti Aliased 3 Operator Parabola Phase Modulation synth..............................24

2
AA of Oscillators and Distortions with Pre Integrated WaveTables T.Rochebois

2 A simple Wavetable Oscillator


2.1 Wave tables, How to read them

A wave table consists in a table that contains a set of samples that can be looped. The samples
represent a cycle of the signal. "Samples" means that values "v" of the analog signal have been
picked at regular time intervals.

A wavetable is very handy, it allows to access any value of the wave at any moment. For example,
let's say that we want to know what's the value of the wavetable at position 7, we can tell it directly
by accessing the table. v[7]

Now, we want to know the value at 7.3, how can we do that ?

To get an idea of what happens between two samples we must interpolate. The straightforward
interpolation method is linear interpolation, we just connect the dots.

We guess that the value is somewhere between the values v[7] and v[8]. Linear interpolation
consists in mixing those two values:

v(7.3) = v[7]*(1-0.3) + v[8]*0.3

which can be rewritten as

v(7.3) = v[7] + 0.3 * dv[7],


with dv[7] = v[8] - v[7], being the slope between sample 7 and sample 8.

3
AA of Oscillators and Distortions with Pre Integrated WaveTables T.Rochebois

2.2 Digital Oscillators, Phases and Frequencies

An oscillator oscillates i.e. it cycles at a specific frequency.


To do so, the most convenient way, in the digital domain, consists in using a phase accumulator
"p" that will remember where we are in the cycle.

p will be incremented at every tick of the main sampling rate (you know, 44.1kHz or 48KHz aka
srate in jsfx).

The rate at which it will be incremented "dp" controls the frequency of the oscillator.

It consists in a single line of code:


p += dp;

Oups, i forgot that an oscillator has to cycle, here "p" goes away. We can fix a limit to "p" and tell
him to go back.
Let's say we want it to stay in the [0 16[ range, we can code:

p += dp;
p >= 16 ? p -= 16;

2.3 Digital Oscillators, Accessing the wavetable

Now, we simply have to access the table with our phase:

p += dp; // phase increment


p >= 16 ? p -= 16; // phase cycling
p0 = floor(p); // integer part of the phase
a = p - floor(p); // float part of the phase (for linear interpolation)
v[p0] + a * dv[p0];

4
AA of Oscillators and Distortions with Pre Integrated WaveTables T.Rochebois

2.4 Digital Hell: Wavetable Aliasing

Everything sounds good so far, but what happens if we read our wavetable faster ?

We miss some details and get some others from the original waveform, depending on the position of
the phase accumulator for each iteration.

And even worse, we don't miss or get the same details at each cycle of the oscillator. The oscillator's
output is no more cyclic, there is some aperiodic dirt in the high end of the spectrum. Some say it is
metallic and cold, digital cold.

5
AA of Oscillators and Distortions with Pre Integrated WaveTables T.Rochebois

3 The popular cure: MIP MAPs


Aliasing occurs when you read a wavetable with lots of details too fast.
The MIPMAP idea is simple: remove the details when you want to read fast !

It's hard to remove details on the fly, so, the solution consists in preparing a set of wavetables (the
MIPMAP). Each wavetable of the MIPMAP set contains just the right amount of details for a
certain range of frequencies (i.e. MIDI notes).
The main drawback is that it uses lots of memory, but today, memory is cheap. So...

This is the method i use for the PWT_synth. It is a very efficient method and the most popular
antialiasing method for wavetables so far.

4 The old trick: Pre integration

4.1 Memories
Pre integration is a trick I used in the 90s. The computers I used for my thesis work were not very
powerful and had much less memory than an average laptop of 2016. As my work implied multiple
wavetables, I had to find a cure to my wavetable aliasing problem. Pre integration was the method I
opted for.

4.2 Back to the problem


So let's go back to how we read wavetables.

The problem is that we read what is at a certain point in the wavetable.

Ideally, we should read what is in a certain region of the wavetable, the "what happened since last
time region".

The "what happened since last time region"

4.3 Oversampling
Sure, but how can we do that ? How can we know what was in the "what happened since last time
region".
Well, we can cheat and take more samples - that's called oversampling - but it will cost some CPU

6
AA of Oscillators and Distortions with Pre Integrated WaveTables T.Rochebois

and we have to guess how many oversamples we need.

4.4 Area

How can we get the mean value of the "what happened since last time region" instead of a ponctual
value taken in this region ?
Has far as i learnt in high school, the mean value is the area under the curve devided by the width of
the region.

It's something like


mean value = A/dp
A is the area.
dp is the phase increment (proportional to the frequency).

How can we calculate this area ?

4.5 Integral

An area under a function can be exactly calculated by substracting its integrand at the limit points.

Say Iv is the integrand of v, we have


A = Iv(p) - Iv(previous p)
i.e.
A = Iv(p) - Iv(p - dp)

So the mean value is


( Iv(p) - Iv(p - dp) ) / dp

4.6 Pre integration

So, we need this nice and handy Iv() function. Sorry, we have to do some maths.

The v() function - the linearly interpolated samples - is made of line segments.

Let's zoom on a line segment, between two samples:

7
AA of Oscillators and Distortions with Pre Integrated WaveTables T.Rochebois

v(p) = v[p0] + a * dv[p0];


p0 being the integer part of p and a being the float part.

A function Iv(p) that has v(p) as its derivative is something like:


Iv(p) = Iv[p0] + a * v[p0] + 1/2 a^2 dv[p0]

It is a second order polynomial segment.


The Iv() function is made of second order polynomial segments.
These polynomial segments are defined by the values contained in tables v[], dv[] and the new table
Iv[].
The new table Iv[] simply contains offset values that ensures continuity between segments. This
looks like a detail but it is mandatory: it ensures that the polynomial segments still connects the
dots.

4.7 A perfect world ?

Pre integration allows to calculate the mean value of our wavetable on any region by substracting to
values and dividing the result by the width of the region.
It is far superior to the trivial linear interpolation scheme.

But it is not as good as MIPMAPs. Why is that so ?

Sure, it gives the mean value of the "what happened since last time region", but that mean value is
far from a perfect anti aliasing filter.

A mean value provides what's called a "box filter". The box filter frequency response is a sinc
function (DSP Related:Running Sum Lowpass Filter). Let's say that it cannot rivalise with Fourier
band limiting used in MIPMAPs.

Box filtering is a smoothing filter, Fourier band limiting is a brick wall.

That's why Pre integration should not be seen as a perfect antialiasing scheme. To be efficient it can
be used with other methods such as oversampling. In this case, it can be as effective as MIPMAPing
and more importantly it can be extended beyond wavetables.

5 Cousins

5.1 Differentiated Parabolic Waveforms

Pre integration of wavetables have some cousins: the DPWs, Differenciated Parabolic Waveforms.

A DPW oscillator does not use wave tables. It generates a parabolic waveform based on its phase.
This parabola is differentiated to generate a somewhat band limited sawtooth. Other waveforms,
such as square and rectangle are generated by combining sawteeth.

8
AA of Oscillators and Distortions with Pre Integrated WaveTables T.Rochebois

DPW were used in the late 80s in digital synthesizers.

5.2 Differentiated Polynomial Waveforms

In the late 90s a group of my students worked at a final year project. This final year project
consisted in coding a simple band limited oscillator without wavetables. The generator should
exclusively be based on math functions.

I suggested the idea of Parabolic Waveforms and that they may experiment with higher order
Polynomials and higher order differentiators.

The results were quite good: low aliasing and low CPU. The "box filter" implied by the first order
differentiator was replaced by something like "the box filter of a box filter of a box filter"... which
starts to look like a gaussian filter.

The main drawback of the method is that it needs high accuracy. Floats were enough for a third
order integration/differentiation scheme. Higher order schemes needed doubles.

See, for example, this old thread on the KVR DSP forum:
http://www.kvraudio.com/forum/viewtopic.php?p=1710116#1710116

Later i used this method in a VSTi synth you may find somewhere on the net "The Vagabond King".
The Vagabond King VSTi

6 Back to Pre integrated wavetables

6.1 Higher order pre integrated wavetables

The case of wavetables is more sensitive to accuracy than the generation of sawteeth by the DPW
method. Instead of a polynomial, you have to deal with a set of polynomial segments and ensure
that calc errors are kept low.

Nevertheless, today, performing maths with 64bit doubles is not a big deal. So I can use a second or
third order integration/differentiation scheme on wavetables without problems.

I use this method in the TiaR_Ze_Cheesy_Synth.jsfx, it is a jsfx plugin for Reaper (available from
the Reapack/ReaTeam repository).

The main advantage VS mipmaping is that the wavetable pre integration process is much simpler
than MIPMAPing.

9
AA of Oscillators and Distortions with Pre Integrated WaveTables T.Rochebois

6.2 Anti Aliased Distortion

Pre integrated tables can be used beyond wavetables. In areas not available to MIPMAPing.

One such application is distortion.


A distortion is a non linear function of the input signal.
The non linearity produce lots of overharmonics (niiice) and lots of aliasing (arrrg).

Many years ago i tried to use dynamic MIPMAPing i.e. switching or crossfading between
MIPMAP levels depending on the slew rate of the input... The crossfading induced aliasing by
itself.

So let's see how we can use pre integrated tables to get an anti aliased distortion.

It's much like a wavetable, but instead of accessing the table with a phase we will access it with the
input signal. We have to go from a serial access to a random access.

I tried it, it works. But as the input signal is much much less predictable than a phase, calc accuracy
is quite delicate. Anyway, I achieved nice results using a second order scheme even with
complicated distortion. (for example: Pre integrated Dissymetric smooth "tube" saturation).

Second Order pre integration + oversampling by two may be an effective and elegant solution to the
problem of aliasing in non linearities.

6.3 Anti Aliased Phase Modulation

Distortion is a special case of phase modulation. It is the case were the frequency of the oscillator is
zero.

So, based on my experiments with anti aliasing distortion, i went a step further.

Generaly speaking, phase modulation (or frequency modulation) implies sine oscillators. Phase
modulation of complex wavetable oscillators often generate too many harmonics and induce strong
aliasing. A second orderpre integration allows to implement such a modulation. See Anti aliased 3
operator PM synth. for an operational implementation (detailed information in the appendix).

10
AA of Oscillators and Distortions with Pre Integrated WaveTables T.Rochebois

Appendix A Commented JSFX code examples


These are fully operational JSFX plugins you can test and use under Reaper.

Appendix A.1 An Anti Aliased Walsh Function synth


This JSFX synth shows how a third order differentiation scheme can be used to anti alias a wave
table oscillator.
Ze Cheesy Harmonic Synth is a small JSFX synth reminiscent of early digital additive synthesizers.
(It is available for Reaper from ReaPack). In this "Harmonic" synth, instead of adding sine waves,
the basic waveforms are Walsh sequences: sort of square waves. This JSFX synth is anti aliased
thanks to the third order integration differentiation scheme i extended to wave tables.
Here is the complete listing:
/**
* JSFX Name:ZeCheesyHarmo_01
* About:
* An additive synth based on Walsh functions
* Author: T.Rochebois
* Licence: LGPL
* REAPER: 5.0
* Version: 0.3
*/

desc:Ze Cheesy Harmonic Synth 01

slider1:1<-1,1,0.001>Seq. 1
slider2:0<-1,1,0.001>Seq. 2
slider3:0<-1,1,0.001>Seq. 3
slider4:0<-1,1,0.001>Seq. 4
slider5:0<-1,1,0.001>Seq. 5
slider6:0<-1,1,0.001>Seq. 6
slider7:0<-1,1,0.001>Seq. 7
slider8:0<-1,1,0.001>Seq. 8
slider9:0<-1,1,0.001>Seq. 9
slider10:0<-1,1,0.001>Seq. 10
slider11:0<-1,1,0.001>Seq. 11
slider12:0<-1,1,0.001>Seq. 12
slider13:0<-1,1,0.001>Seq. 13
slider14:0<-1,1,0.001>Seq. 14
slider15:0<-1,1,0.001>Seq. 15
slider16:0<-1,1,0.001>Seq. 16

slider20:0<-24,24,0.0001>Detune (semitones)
slider21:5.5<1,20>Vibrato rate
slider22:0.4<0,1,0.0001>Vibrato depth
slider23:1<0,1,0.0001>Glide rate
slider24:0<0,1>Tremolo depth

slider31:-3<-3,1,0.0001>Attack
slider32:0<-3,1,0.0001>Decay
slider33:1<0,1,0.0001>Sustain
slider34:-2<-3,1,0.0001>Release

11
AA of Oscillators and Distortions with Pre Integrated WaveTables T.Rochebois

slider41:0.5<0,1,0.0001>Pan
slider42:0<-24,12>Gain (dB)
// ___________________________________________________________________
@init
//_____________________________________________________________________
function ADSR_setP10(A D S R) (
// decay coefs ref time(s)
this.A = 1 - exp(log(0.33) / ((10^A) * (srate/KRATE)));
this.A = min(this.A, 1);
this.D = 1 - exp(log(0.10) / ((10^D) * (srate/KRATE)));
this.D = min(this.D, 1);
this.S = S;
this.R = 1 - exp(log(0.10) / ((10^R) * (srate/KRATE)));
this.R = min(this.R, 1);
);
//_____________________________________________________________________
// Control rate processing
function ADSR_kProc(gate trig)
local() (
gate === 0 ? this.AttackSeg = 0;
trig ? this.AttackSeg = 1;
this.ASR < gate ?
this.ASR += this.A * (gate - this.ASR)
: this.ASR += this.R * (gate + 0.00000001 - this.ASR);
// _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
this.AttackSeg === 1 ? (
this.ADSR += this.A * (1.5 - this.ADSR);
this.ADSR >= 1 ?
( this.ADSR = 1.0;
this.AttackSeg = 0;
);
);
this.AttackSeg === 0 ? (
gate != 0 ? (
this.ADSR += this.D * (this.S - this.ADSR);
) : (
this.ADSR += this.R * (0.00000001 - this.ADSR);
);
);
this.ASR * this.ADSR;
);
// ____________________________________________________________________
function WSH_init()
instance(sal) local(seq x x0 x1 x2 x3 x4)(
sal = ad; ad += 32*16;
x = 0; loop(32,
x0 = (x& 1) ?1:-1;
x1 = ((x& 2)>>1)?1:-1;
x2 = ((x& 4)>>2)?1:-1;
x3 = ((x& 8)>>3)?1:-1;
x4 = ((x&16)>>4)?1:-1;
sal[x + 32 * 0] = x4;
sal[x + 32 * 1] = x3 ;
sal[x + 32 * 2] = x2*x3*x4;
sal[x + 32 * 3] = x2 ;
sal[x + 32 * 4] = x1*x2* x4;
sal[x + 32 * 5] = x1*x2*x3 ;
sal[x + 32 * 6] = x1 *x3*x4;
sal[x + 32 * 7] = x1 ;

12
AA of Oscillators and Distortions with Pre Integrated WaveTables T.Rochebois

sal[x + 32 * 8] = x0*x1 *x4;


sal[x + 32 * 9] = x0*x1 *x3 ;
sal[x + 32 * 10] = x0*x1*x2*x3*x4;
sal[x + 32 * 11] = x0*x1*x2 ;
sal[x + 32 * 12] = x0 *x2 *x4;
sal[x + 32 * 13] = x0 *x2*x3 ;
sal[x + 32 * 14] = x0 *x3*x4;
sal[x + 32 * 15] = x0 ;
x += 1;
);
);
function PIWT_init(nMax) instance(n x y z w dp _dp _dp3) (
this.nMax = nMax;
n = nMax;
x = ad; ad += n;
y = ad; ad += n;
z = ad; ad += n;
w = ad; ad += n;
dp = n*440/srate;
_dp = 1 / dp;
_dp3 = _dp * _dp * _dp;
);
// _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
function PIWT_dcRemove(t n) local(i m)(
m = 0; i = 0; loop(n, m += t[i]; i += 1; );
m /= n; i = 0; loop(n, t[i] -= m; i += 1; );
);
// _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
function PIWT_norm(t n) local(i m)(
m = 0; i = 0; loop(n, m = max(t[i], abs(m)); i += 1; );
m = 1.1/(m+0.1); i = 0; loop(n, t[i] *= m; i += 1; );
);
// _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
function PIWT_integ()
instance(x n y z w z y x)
local(i)(
PIWT_dcRemove(x, n);
i = 0; loop(n-1, y[i+1] = y[i] + x[i]; i += 1; );
PIWT_dcRemove(y, n);
i = 0; loop(n-1, z[i+1] = z[i] + y[i] + (1/2) * x[i]; i += 1; );
PIWT_dcRemove(z, n);
i = 0; loop(n,
y[i] *= 1/2;
x[i] *= 1/6;
i < n-1 ? w[i+1] = w[i] + z[i] + y[i] + x[i];
i += 1;
);
PIWT_dcRemove(w, n);
);
// _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
// Call before aProc if there is a discontinuity of dp _dp
function PIWT_disc()
instance(n dp _dp _dp3 p out w z y x w0 w1 w2) local(p0 a)(
p -= 2*dp; p += n * (p < 0); p -= n * (p >= n); p0 = p|0; a = p - p0;
w0 = w[p0] + a * (z[p0] + a * (y[p0] + a * x[p0]));

p += dp; p -= n*(p>=n); p0 = p|0; a = p-p0;


w1 = w[p0] + a * (z[p0] + a * (y[p0] + a * x[p0]));

13
AA of Oscillators and Distortions with Pre Integrated WaveTables T.Rochebois

p += dp; p -= n*(p>=n); p0 = p|0; a = p-p0;


w2 = w[p0] + a * (z[p0] + a * (y[p0] + a * x[p0]));
);
// _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
function PIWT_aProc()
instance(n dp _dp _dp3 p out w z y x w0 w1 w2 w3) local(p0 a)(
p += dp; p -= n*(p>=n); p0 = p|0; a = p-p0;
w3 = w[p0] + a * (z[p0] + a * (y[p0] + a * x[p0]));
out = (w3 - w0 + 3 * (w1 - w2)) * _dp3;
w0 = w1; w1 = w2; w2 = w3;
out;
);
// ___________________________________________________________________
// Init the wave table
_srate = 1 / srate;
KRATE = 8; _KRATE = 1/KRATE;
piwt.PIWT_init(32);
wsh.WSH_init();
kbNotes = ad; ad += 128;
bendFactor = bendFactorF = 1;
// ___________________________________________________________________
@slider
gain = 2^((1/6)*slider42);
gLeft = gain * cos(0.5*$pi*slider41);
gRight = gain * sin(0.5*$pi*slider41);
dpLfo = 2*$pi*slider21*32*_srate;
vibMin = slider22*0.05;
vibMax = 0.025 + 2 * vibMin;
adsr.ADSR_setP10(slider31, slider32, slider33, slider34);
glideCoef = 32 * _srate * (0.1 + 100 * slider23 * slider23);
x = 0; loop(32,
piwt.x[x] = slider1 * wsh.sal[x + 32 * 0];
seq = 1;loop(15,
piwt.x[x] += slider(seq + 1) * wsh.sal[x + 32 * seq];
seq += 1;
);
x += 1;
);
PIWT_dcRemove(piwt.x,32);
PIWT_norm(piwt.x,32);
piwt.PIWT_integ();
piwt.PIWT_disc();
// ___________________________________________________________________
@block
while (midirecv(offset, msg1, msg23)) (
msg2 = msg23 & 0x7F; msg3 = msg23 >> 8; status = msg1 & $xF0;
status == $x80 ? ( status = $x90; msg3 = 0; ); // note off
status == $x90 ? ( // note on
msg3 == 0 ? (
kbNotes[msg2] = 0;
gate = 0; i=0; loop(128, kbNotes[i] !== 0 ? (gate = kBNotes[i]; note=i;);
i+=1;);
gate ? dpc = (piwt.n*440*_srate) * 2 ^ ((note + slider20 - 69) * (1/12));
) : (
gate == 0 ? trig = 1;
kbNotes[msg2] = gate = sqrt(msg3 * (1/127));
note = msg2;
dpc = (piwt.n*440*_srate) * 2 ^ ((note + slider20 - 69) * (1/12));
trig ? (dpg = dpc;);

14
AA of Oscillators and Distortions with Pre Integrated WaveTables T.Rochebois

);
)
: status == $xB0 ? (
msg2 == 1 ? modWhl = msg3 * (1/127);

// All notes off


msg2 == 123 ? (trig = gate = 0;i=0; loop(128, kBNotes[i] = 0; note=i;);
i+=1;);
)
: status == $xD0 ? (aftertouch = msg2 * (1/127);)
: status === 14 * 16 ? (
bend = (msg3 << 7 | msg2) - 8192;
bend <= 0 ? bend *= 1 / 8192 : bend *= 1 / 8191;
bendFactor = 2^bend;

);
co_vib = vibMin + modWhl * (vibMax - vibMin);
co_vibA = max(co_vib, vibMin + aftertouch * (vibMax - vibMin));

midisend(offset, msg1, msg23);


);
// ___________________________________________________________________
@sample
k2 <= 0 ? (
k2 = KRATE;
dEnv = (adsr.ADSR_kProc(gate, trig) - env)*_KRATE;
trig = 0;
);
k2 -= 1;
// _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
k <= 0 ? (
k = 32;
bendFactorF += 0.05 * (bendFactor - bendFactorF);
slider23 >=1 ? dpg = bendFactorF * dpc
: dpg += glideCoef * (bendFactorF * dpc - dpg);
pLfo += dpLfo; pLfo -= 2*$pi*(pLfo>0);
lfo = sin(pLfo);
dTrem = (1/32)*( 1 + slider24 * (0.5*lfo+0.5 - 1) - trem);
co_vibAf += 0.05 * (co_vibA - co_vibAf);
piwt.dp = dpg * (1 + co_vibAf * lfo);
piwt._dp = 1 / piwt.dp;
piwt._dp3 = piwt._dp * piwt._dp * piwt._dp;
piwt.PIWT_disc();
);
k -= 1;
// _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
env += dEnv;
trem += dTrem;
out = min(10,max(-10,trem*env*piwt.PIWT_aProc()));
spl0 += gLeft * out;
spl1 += gRight * out;
// ___________________________________________________________________
@gfx 0 64
function col(r g b a) (gfx_r = r; gfx_g = g; gfx_b = b; gfx_a = a;);

col(0.0,0,0.5,1);
gfx_rect(0,0,32*12+16, 64);

col(1.0,1.0,1.0,1);

15
AA of Oscillators and Distortions with Pre Integrated WaveTables T.Rochebois

gi = 0; loop(32,
gx = gi*12+8;
gi>0?
gfx_line(gx, 32-144*piwt.x[gi-1], gx, 32-144*piwt.x[gi])
:
gfx_line(gx, 32, gx, 32-144*piwt.x[gi]);
gfx_line(gx, 32-144*piwt.x[gi], gx+12, 32-144*piwt.x[gi]);
gi+=1;
);
gx = gi*12+8;
gfx_line(gx, 32-144*piwt.x[31], gx, 32);

I will comment and describe part of this code.

desc:Ze Cheesy Harmonic Synth

slider1:1<-1,1,0.001>Seq. 1
slider2:0<-1,1,0.001>Seq. 2
slider3:0<-1,1,0.001>Seq. 3
slider4:0<-1,1,0.001>Seq. 4
slider5:0<-1,1,0.001>Seq. 5
slider6:0<-1,1,0.001>Seq. 6
slider7:0<-1,1,0.001>Seq. 7
slider8:0<-1,1,0.001>Seq. 8
slider9:0<-1,1,0.001>Seq. 9
slider10:0<-1,1,0.001>Seq. 10
slider11:0<-1,1,0.001>Seq. 11
slider12:0<-1,1,0.001>Seq. 12
slider13:0<-1,1,0.001>Seq. 13
slider14:0<-1,1,0.001>Seq. 14
slider15:0<-1,1,0.001>Seq. 15
slider16:0<-1,1,0.001>Seq. 16

[...]

slider41:0.5<0,1,0.0001>Pan
slider42:0<-24,12>Gain (dB)

This is the header section which defines the sliders: Digital "Harmonics" amplitudes, Detune and
Vibrato, Enveloppe and Pan controls are defined here.

// ___________________________________________________________________
@init
//_____________________________________________________________________
[...]

The @init section contains function declarations and init code such as the ADSR enveloppe code.
We won't focus on that, we'd rather look for what's specific to this synth.

Next, we have the Walsh function code:

16
AA of Oscillators and Distortions with Pre Integrated WaveTables T.Rochebois

// ____________________________________________________________________
function WSH_init()
instance(sal) local(seq x x0 x1 x2 x3 x4)(
sal = ad; ad += 32*16;
x = 0; loop(32,
x0 = (x& 1) ?1:-1;
x1 = ((x& 2)>>1)?1:-1;
x2 = ((x& 4)>>2)?1:-1;
x3 = ((x& 8)>>3)?1:-1;
x4 = ((x&16)>>4)?1:-1;
sal[x + 32 * 0] = x4;
sal[x + 32 * 1] = x3 ;
sal[x + 32 * 2] = x2*x3*x4;
sal[x + 32 * 3] = x2 ;
sal[x + 32 * 4] = x1*x2* x4;
sal[x + 32 * 5] = x1*x2*x3 ;
sal[x + 32 * 6] = x1 *x3*x4;
sal[x + 32 * 7] = x1 ;
sal[x + 32 * 8] = x0*x1 *x4;
sal[x + 32 * 9] = x0*x1 *x3 ;
sal[x + 32 * 10] = x0*x1*x2*x3*x4;
sal[x + 32 * 11] = x0*x1*x2 ;
sal[x + 32 * 12] = x0 *x2 *x4;
sal[x + 32 * 13] = x0 *x2*x3 ;
sal[x + 32 * 14] = x0 *x3*x4;
sal[x + 32 * 15] = x0 ;
x += 1;
);
);

That initialises the 16 walsh function tables. These tables will be added together in the Pre
Integrated Wave Table everytime a slider is moved.

Here come the Pre Integrated Wave Table functions.

First, the init function:


function PIWT_init(nMax) instance(n x y z w dp _dp _dp3) (
this.nMax = nMax;
n = nMax;
x = ad; ad += n;
y = ad; ad += n;
z = ad; ad += n;
w = ad; ad += n;
dp = n*440/srate;
_dp = 1 / dp;
_dp3 = _dp * _dp * _dp;
);

it initializes a pre integrated wave function.


x, y, z and w are the tables that will contain the pre integrated values. ("ad" is an address counter
that keeps track of the memory usage in the plugin).

17
AA of Oscillators and Distortions with Pre Integrated WaveTables T.Rochebois

Then we have some utility functions.


// _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
function PIWT_dcRemove(t n) local(i m)(
m = 0; i = 0; loop(n, m += t[i]; i += 1; );
m /= n; i = 0; loop(n, t[i] -= m; i += 1; );
);

This is a utility function that removes the mean value (DC) of a table. It is an important feature
when you integrate periodic tables more than once: it guaranties that the integrals will be periodic
too.

// _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
function PIWT_norm(t n) local(i m)(
m = 0; i = 0; loop(n, m = max(t[i], abs(m)); i += 1; );
m = 1.1/(m+0.1); i = 0; loop(n, t[i] *= m; i += 1; );
);

This is another utility function that roughly normalize the content of a table.

One of the core function is:


// _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
function PIWT_integ()
instance(x n y z w z y x)
local(i)(
PIWT_dcRemove(x, n);
i = 0; loop(n-1, y[i+1] = y[i] + x[i]; i += 1; );
PIWT_dcRemove(y, n);
i = 0; loop(n-1, z[i+1] = z[i] + y[i] + (1/2) * x[i]; i += 1; );
PIWT_dcRemove(z, n);
i = 0; loop(n,
y[i] *= 1/2;
x[i] *= 1/6;
i < n-1 ? w[i+1] = w[i] + z[i] + y[i] + x[i];
i += 1;
);
PIWT_dcRemove(w, n);
);

It is the pre integration method that prepares the x, y, z and w tables.

The input table is x. It consists of 32 steps, a mixture of Walsh functions, no linear interpolation.
First of all, we remove its mean value by calling the utility function PIWT_dcRemove(x, n);

Then we calculate its first integral


i = 0; loop(n-1, y[i+1] = y[i] + x[i]; i += 1; );

18
AA of Oscillators and Distortions with Pre Integrated WaveTables T.Rochebois

and we remove its DC bias too with PIWT_dcRemove(y, n);

With x and y we can calculate I1, the first integral.


Its value in segment number p0 is:
I 1 ( p)=I 1 (a) p = y [ p0 ]+a x [ p 0 ]
0

"p0" being the integer part of p and "a" being its float part.

The second order integral z is


i = 0; loop(n-1, z[i+1] = z[i] + y[i] + (1/2) * x[i]; i += 1; );

Why is the x[i] present, we should accumulate y[i] ?


Why does the x[i] have a (1/2) coefficient ?
Why do we accumulate z[i] to z[i+1]

So let's see what happens in a segment.


In the segment number p0, we have
1 2
I 2 ( p)=I 2 (a) p =z [ p 0 ]+a y [ p 0 ]+ a x [ p0 ]
0
2
its derivate is
dI 2 ( p)
=I 1 ( p)= y [ p0 ]+a x [ p0 ]
dp
It does not depend on z[p0]. This relationship between I2 and I1 will still be true even if we did not
accumulate the previous values of z.

Accumulating z values ensures that the polynomial segments "connect the dots".
At the boundaries, we have
I 2 ( p0 + )I 2 ( p 0)
i.e.
1 1
z [ p0 ]+ y [ p 0]+ 2 x [ p0 ]z [ p01]+(1) y [ p 01]+ (1)2 x [ p01]
2 2
The limit of this equation gives us our integration code.

The last step calculates w, the third order integral. It is mostly similar to the previous step, so i won't
go into it.

19
AA of Oscillators and Distortions with Pre Integrated WaveTables T.Rochebois

Nota: I scale x and y with 1/2 and 1/6 factors. It is an optimisation that will be used in
PIWT_aProc()

// _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
function PIWT_aProc()
instance(n dp _dp _dp3 p out w z y x w0 w1 w2 w3) local(p0 a)(
p += dp; p -= n*(p>=n); p0 = p|0; a = p-p0;
w3 = w[p0] + a * (z[p0] + a * (y[p0] + a * x[p0]));
out = (w3 - w0 + 3 * (w1 - w2)) * _dp3;
w0 = w1; w1 = w2; w2 = w3;
out;
);

This is the core function of the synth.


instance(n dp _dp _dp3 p out w z y x w0 w1 w2 w3)
are the state variables of the oscillator
n is the table size
dp is the phase increment (frequency)
_dp is the reciprocal of the phase increment
_dp3 is the cube of the reciprocal of the phase increment. It is used
to normalize the differentiation
p is the phase
x y z w are the polynomial tables
w0 w1 w2 w3 are delayed values of the polynomial

p += dp; p -= n*(p>=n);
This is the phase increment and modulo (we stay in [0, n[ )

p0 = p|0; a = p-p0;
Splits p into its integer and float parts

w3 = w[p0] + a * (z[p0] + a * (y[p0] + a * x[p0]));


Calculates the third order integral of the point
Note:
it should be
w[p0] + a * z[p0] + a*a * (1/2)*y[p0] + a*a*a * (1/6)*x[p0];
but I already scaled y by (1/2) and x by (1/6).
Another optimisation is that I put the polynomial in its Horner form.
So that
w3 = w[p0] + a * (z[p0] + a * (y[p0] + a * x[p0]));

20
AA of Oscillators and Distortions with Pre Integrated WaveTables T.Rochebois

costs only three multiplications and three additions.


out = (w3 - w0 + 3 * (w1 - w2)) * _dp3;
The output is the third order differentiation normalized by the cube of the phase increment.

// _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
// Call before aProc if there is a discontinuity of dp _dp
function PIWT_disc()
instance(n dp _dp _dp3 p out w z y x w0 w1 w2) local(p0 a)(
p -= 2*dp; p += n * (p < 0); p -= n * (p >= n); p0 = p|0; a = p - p0;
w0 = w[p0] + a * (z[p0] + a * (y[p0] + a * x[p0]));

p += dp; p -= n*(p>=n); p0 = p|0; a = p-p0;


w1 = w[p0] + a * (z[p0] + a * (y[p0] + a * x[p0]));

p += dp; p -= n*(p>=n); p0 = p|0; a = p-p0;


w2 = w[p0] + a * (z[p0] + a * (y[p0] + a * x[p0]));
);

This function updates the state variables of an oscillator. It should be


called every time there is a discontinuity of dp to prevent glitches.

[...]
// ___________________________________________________________________
@slider
[...]
x = 0; loop(32,
piwt.x[x] = slider1 * wsh.sal[x + 32 * 0];
seq = 1;loop(15,
piwt.x[x] += slider(seq + 1) * wsh.sal[x + 32 * seq];
seq += 1;
);
x += 1;
);
PIWT_dcRemove(piwt.x,32);
PIWT_norm(piwt.x,32);
piwt.PIWT_integ();
piwt.PIWT_disc();

The @slider section takes in charge the update of the Pre Integrated
Wave Table.

First it is updated as a mixture of Walsh functions:


x = 0; loop(32,
piwt.x[x] = slider1 * wsh.sal[x + 32 * 0];
seq = 1;loop(15,
piwt.x[x] += slider(seq + 1) * wsh.sal[x + 32 * seq];
seq += 1;
);

21
AA of Oscillators and Distortions with Pre Integrated WaveTables T.Rochebois

x += 1;
);

Then the piwt.x table is normalized and pre Integrated:


PIWT_dcRemove(piwt.x,32);
PIWT_norm(piwt.x,32);
piwt.PIWT_integ();

Glitches are prevented by calling


piwt.PIWT_disc();

// ___________________________________________________________________
@block
while (midirecv(offset, msg1, msg23)) (
[...]
);

Nothing special with the @block : it handles MIDI control

Let's have a look to the @sample section.

// ___________________________________________________________________
@sample
k2 <= 0 ? (
k2 = KRATE;
dEnv = (adsr.ADSR_kProc(gate, trig) - env)*_KRATE;
trig = 0;
);
k2 -= 1;

This is a "control rate" sub-section. This code is run every KRATE sample. This kind of
optimisation saves lots of CPU without compromising sound quality if you choose KRATE
correctly.
// _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
k <= 0 ? (
k = 32;
bendFactorF += 0.05 * (bendFactor - bendFactorF);
slider23 >=1 ? dpg = bendFactorF * dpc
: dpg += glideCoef * (bendFactorF * dpc - dpg);
pLfo += dpLfo; pLfo -= 2*$pi*(pLfo>0);
lfo = sin(pLfo);
dTrem = (1/32)*( 1 + slider24 * (0.5*lfo+0.5 - 1) - trem);
co_vibAf += 0.05 * (co_vibA - co_vibAf);
piwt.dp = dpg * (1 + co_vibAf * lfo);
piwt._dp = 1 / piwt.dp;
piwt._dp3 = piwt._dp * piwt._dp * piwt._dp;

22
AA of Oscillators and Distortions with Pre Integrated WaveTables T.Rochebois

piwt.PIWT_disc();
);
k -= 1;

This other sub-section is run every 32 samples, it contains updates of the various MIDI controls that
affect the pitch of the sound. Here piwt.dp, piwt._dp and piwt._dp3 are updated and
piwt.PIWT_disc() is called to avoid glitches.

// _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
env += dEnv;
trem += dTrem;
out = min(10,max(-10,trem*env*piwt.PIWT_aProc()));
spl0 += gLeft * out;
spl1 += gRight * out;

This code is run for every sample. The enveloppe and tremolo are linearly interpolated.
env += dEnv;
trem += dTrem;

The output is calculated with the 3rd order differentiator scheme:


out = min(10,max(-10,trem*env*piwt.PIWT_aProc()));

Appendix A.2 An Anti Aliased 3 Operator Parabola Phase


Modulation synth
Here is a mono synth that consist in a chain of three operators. Each operator generates a cyclic
parabola signal that modulates the phase of the operator below it.
The anti aliasing consist of a second order integration/diff extendent to phase modulation.

Here is the complete JSFX code:

desc:Parabola Phase Modulation Synthesizer


// Author: T.Rochebois 08/2016
slider3:sl_R2=1.0001<0,3,0.000001>Ratio 2
slider4:sl_I2=1.5<0,3,0.0001>I2

slider6:sl_R1=0.9997<0,3,0.000001>Ratio 1
slider7:sl_I1=0.5<0,3,0.0001>I1

slider9:sl_R0=1<0,3,0.000001>Ratio 0

slider11:sl_A=-3<-3,0,0.001>Attack
slider12:sl_R=0<-2,1,0.001>Release
// ___________________________________________________________________

23
AA of Oscillators and Distortions with Pre Integrated WaveTables T.Rochebois

@init
function I0(x)( x+=16;x-=x|0; (6*x-6)*x+1 );
function I1(x)( x+=16;x-=x|0; ((2*x-3)*x+1)*x );
function I2(x)( x+=16;x-=x|0; ((0.5*x-1)*x+0.5)*x*x );
function AA2_para(dp m)
instance(p x0 x1 y0 y1 I2_0 I2_1 I1_0 I1_1 out)(
p += dp; p >= 1 ? (p -= 1; x0 -= 1; y0 -= 1; );
x1 = x0; I2_1 = I2_0; x0 = p + m;
I2_0 = I2(x0);
y1 = y0; I1_1 = I1_0; y0 = 0.5 * (x0 + x1);
I1_0 = x0 == x1 ? I1(y0) : (I2_0 - I2_1)/(x0 - x1);
out = y0 == y1 ? I0(y0) : (I1_0 - I1_1)/(y0 - y1);
);
// ___________________________________________________________________
@block
while (midirecv(offset, msg1, msg23)) (
msg2 = msg23 & 0x7F; msg3 = msg23 >> 8; status = msg1 & $xF0;
status == $x80 ? ( status = $x90; msg3 = 0; ); // note off
status == $x90 ? ( // note on
msg3 == 0 ? (msg2 == note ? gate = 0;)
: ( gate = sqrt(msg3 * (1/127)); note = msg2;
dp = (440/srate)*2^((note-69)*(1/12));
);
);
midisend(offset, msg1, msg23);
);
dp0 = sl_R0 * dp; dp1 = sl_R1 * dp; dp2 = sl_R2 * dp;
A = 1/(srate*10^sl_A);
R = 1/(srate*10^sl_R);
// ___________________________________________________________________
@sample
env += env>gate ? R*(gate-env) : A*(gate-env);
y2 = osc2.AA2_para(dp2,0);
y1 = osc1.AA2_para(dp1, env*sl_I2 * y2);
y0 = osc0.AA2_para(dp0, (0.5+0.5*env)*sl_I1 * y1);
spl0 = spl1 = min(3,max(-3,env*y0));

That's clearly why I like JSFX: you can test your ideas with very few code lines.

The first section describes the sliders that will control the synth.
The second section is the @init section, it contains stuff the plugin will do at init. It's also the place
where you can declare your functions.
The @block section is usually the place where you process MIDI information.
The @sample section is called for every sample. The stereo output is spl0 and spl1.

In this example, the specific code is in the functions and in the @sample section (colored red). I will
comment those.

function I0(x)( x+=16;x-=x|0; (6*x-6)*x+1 );


This is our parabola, integrated 0 times (hence the I0).

24
AA of Oscillators and Distortions with Pre Integrated WaveTables T.Rochebois

First, the x parameter is restrained to [0,1[ by a modulo function : x+=16;x-=x|0;).


The coefficients of (6*x-6)*x+1 have been calculated so that the mean value of I0(x) over [0,1[ is
equal to zero, this is an important feature for integrating it has a periodic function.

function I1(x)( x+=16;x-=x|0; ((2*x-3)*x+1)*x );


This is our parabola, integrated once (hence the I1).
Same thing as before, the x parameter is retrained to the [0,1[ interval.
The derivative of ((2*x-3)*x+1)*x is (6*x-6)*x+1 . The integration constant have been chosen
so that the mean value of I1(x) over [0,1[ is equal to zero so that it can be integrated one more time
without a DC offset.

function I2(x)( x+=16;x-=x|0; ((0.5*x-1)*x+0.5)*x*x );


This is our parabola, integrated twice (hence the I2).
The second derivative of ((0.5*x-1)*x+0.5)*x*x is (6*x-6)*x+1.
This time, we do not intend to integrate it once more, we do not need its DC offset (aka mean value)
to be zero. So i go for the simplest polynomial.

The function
function AA2_para(dp m)
instance(p x0 x1 y0 y1 I2_0 I2_1 I1_0 I1_1 out)(
p += dp; p >= 1 ? (p -= 1; x0 -= 1; y0 -= 1; );
x1 = x0; I2_1 = I2_0; x0 = p + m;
I2_0 = I2(x0);
y1 = y0; I1_1 = I1_0; y0 = 0.5 * (x0 + x1);
I1_0 = x0 == x1 ? I1(y0) : (I2_0 - I2_1)/(x0 - x1);
out = y0 == y1 ? I0(y0) : (I1_0 - I1_1)/(y0 - y1);
);

is the core of the anti aliased parabola operators.


This function uses the "namespace" facility of jsfx (which is sort of object oriented feature). It is
called in the @sample section for each oscillator osc0, osc1, osc2:
y2 = osc2.AA2_para(dp2,0);
y1 = osc1.AA2_para(dp1, env*sl_I2 * y2);
y0 = osc0.AA2_para(dp0, (0.5+0.5*env)*sl_I1 * y1);

The function has two arguments:


dp: the phase increment
m: the phase modulation signal
The @sample code is straightforward and it will be easy for you to edit it and add/change
oscillators.

Now, let's have a look of its internals.

25
AA of Oscillators and Distortions with Pre Integrated WaveTables T.Rochebois

So...
function AA2_para(dp m)
instance(p x0 x1 y0 y1 I2_0 I2_1 I1_0 I1_1 out)(
p += dp; p >= 1 ? (p -= 1; x0 -= 1; y0 -= 1; );
x1 = x0; I2_1 = I2_0; x0 = p + m;
I2_0 = I2(x0);
y1 = y0; I1_1 = I1_0; y0 = 0.5 * (x0 + x1);
I1_0 = x0 == x1 ? I1(y0) : (I2_0 - I2_1)/(x0 - x1);
out = y0 == y1 ? I0(y0) : (I1_0 - I1_1)/(y0 - y1);
);

instance(p x0 x1 y0 y1 I2_0 I2_1 I1_0 I1_1 out)


Lists all the internal state variables of an oscillator.
p is the phase accumulator, it will be incremented by dp. Its range is [0, 1[
x0 is p+m, the total input phase (i.e. including modulation).
x1 is x delayed by one sample
I2_0 is the second order integral of our parabola taken at x0
I2_1 is the second order integral of our parabola taken at x1
y0 is the mean of x0 and x1 (the value in the middle of x0 and x1)
y1 is y0 delayed by one sample
I1_0 is the first order integral of our parabola taken at y0
I1_1 is the first order integral of our parabola taken at y1
out is the output aka our parabola (aka the zero order integral of our parabola).
Note: in a jsfx function, the last statement is the returned value (here it is "out").

Let's have a look at every line:


p += dp;
Increments the phase
p >= 1 ? (p -= 1; x0 -= 1; y0 -= 1; );
Note: there is no "if" in jsfx, the "?" operator is used instead.
This line means "if p is greater or equal to 1, decrease p, x0 and y0 by one".
This line guaranties that p won't go beyond one.
I also decrease x0 and y0 by the same amount because i need them to be consistent with p (note:
always be careful on that point if you design your own oscillators).
x1 = x0;
x1 is the previous value of x0
I2_1 = I2_0;
I2_1 is the previous value of the second order integral of our parabola.
x0 = p + m;
The new value for x0 is the phase + the modulation input
I2_0 = I2(x0);

26
AA of Oscillators and Distortions with Pre Integrated WaveTables T.Rochebois

The new value for I2_0 is the second order integral of our parabola at x0.
y1 = y0; I1_1 = I1_0; y0 = 0.5 * (x0 + x1);
Same thing as before with the first order integral of our parabola.

I1_0 = x0 == x1 ? I1(y0) : (I2_0 - I2_1)/(x0 - x1);


Is a little tricky.
Note: in jsfx == means "almost equal".
Most of the time, x0 != x1 and we have:
I1_0 = (I2_0 - I2_1)/(x0 - x1);
We take as first order integral at point y0 the mean of the integral between x0 and x1 by
differentiationg the second order integral.

The problem is when x0 is near x1, dividing by zero is a bad option, that's why we have a plan B:
directly calculate I1(y0).

Note that the functions I0 I1 and I2 are consistent (no DC offset, no scale factors, etc). This is
important in order to avoid glitches.

out = y0 == y1 ? I0(y0) : (I1_0 - I1_1)/(y0 - y1);


Is mostly the same as the previous line, if y0 is close to y1, we go to plan B and evaluate the
parabola directly. Otherwise, we go for plan A and we take its mean value in the interval y0 y1.

27
AA of Oscillators and Distortions with Pre Integrated WaveTables T.Rochebois

Appendix A.3 An Anti Aliased Dissymetric Saturation


Tubey Sat is an antialiased Saturation JSFX plugin :
http://forum.cockos.com/showthread.php?t=180951

The Saturation function is a smooth dissymetric function defined in a table. The function is defined
by linear segments. The algorithm is much similar to the one used int Ze Cheezy Harmonic Synth
(even if its wavetable is defined by steps).

Tubey Sat combines x2 interpolation/decimation and a second order integration differentiation


scheme.
As usual, the @init section contains function definitions and initialisations.
init2_3(x) and dec2_96_59(x0 x1) are the interpolator and decimator functions. I won't talk about
these, they are classic FIR design.

function PIT_init(n)(
this.n = n; this.n16 = n * 16;
this.dv = ad; ad += n; this.v = ad; ad += n;
this.Iv = ad; ad += n; this.IIv = ad; ad += n;
);

Allocates some tables for the integration scheme.


v will contain the values
dv will contain the deltas (for linear interpolation)
Iv will contain the first order integral coefficients
IIv will contain the second order integral coefficients.

// _____________________________________________________________________
// (performs the pre integrations)
function PIT_update()
instance(n dv v Iv IIv) local(p dcOffset)(
// _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
// Remove DC offset from the input table
dcOffset = v[0]; p = 1; loop(n - 1, dcOffset += v[p]; p += 1; );
dcOffset /= n; p = 0; loop(n, v[p] -= dcOffset; p += 1; );
// _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
// calc the slope between two values (for linear interpolation)
p = 0; loop(n, dv[p] = v[(p+1) % n] - v[p]; p += 1; );
// _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
// First polynomial integration Iv
dcOffset = Iv[0] = 0; 0; p = 0;
loop(n - 1,
Iv[p+1] = Iv[p] + v[p] + 0.5 * dv[p];
dcOffset += Iv[p+1]; p+=1; );
dcOffset /= n; p = 0; loop(n, Iv[p] -= dcOffset; p += 1;);

28
AA of Oscillators and Distortions with Pre Integrated WaveTables T.Rochebois

// _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
// Second polynomial integration IIv
dcOffset = IIv[0] = 0;
p = 0; loop(n - 1,
IIv[p+1] = IIv[p] + Iv[p] + 0.5 * v[p]+(1/6)*dv[p];
dcOffset += IIv[p+1]; p+=1; );
dcOffset /= n;p = 0;loop(n, IIv[p] -= dcOffset;p+=1;);
);

This function pre integrates the input table v.


First, it removes its mean value to avoid bias during integration (dcOffset).
Then it calculates its delta dv[p] (for linear interpolation).
Then the first and second order integration are performed taking account of v and dv.
Note that they are not integration-by-accumulation, they are polynomial segment integration (hence
the presence of v and dv in the integration code for IIv.

function PIT_I0(p)
instance(n n16 dv v) local(p0 a)(
a = p - (p0 = p|0); // "a" is the float part, "p0" is the integer part
p0 %= n; // modulo
dv[p0] * a + v[p0];
);
returns the I0 i.e. the raw function (with linear interpolation)

function PIT_I1(p)
instance(n n16 dv v Iv) local(p0 a)(
a = p - (p0 = p|0);
p0 %= n;
( dv[p0] * 0.5*a + v[p0]) * a + Iv[p0];
);
returns the first integration value I1(p)

function PIT_I2(p)
instance(n n16 dv v Iv IIv) local(p0 a)(
a = p - (p0 = p|0);
p0 %= n;
(( dv[p0] * (1/3)*a + v[p0]) * 0.5*a + Iv[p0]) * a + IIv[p0];
);
returns the second integration value I2(p)

In a pure second order differentiation scheme we should only use PIT_I2.


PIT_I1 and PIT_I0 are "plan B" functions that allows to deal with delicate situations... it is much
similar to what I have done in the PM parabola synth.

29
AA of Oscillators and Distortions with Pre Integrated WaveTables T.Rochebois

Here is the core function of the distortion effect:


function PIO_aProc2(m)
instance(p n x1 x2 I2_1 I2_2 y0 y1 I1_0 I1_1 out)(
x1 = x2; I2_1 = I2_2; x2 = m; I2_2 = this.PIT_I2(x2);
y0 = y1; I1_0 = I1_1; y1 = 0.5 * (x2 + x1);
I1_1 = abs(x2 - x1)<0.0001 ? this.PIT_I1(y1) : (I2_2 - I2_1) / (x2 -
x1);
out = abs(y1 - y0)<0.01 ? this.PIT_I0(0.5*(y0+y1)) : (I1_1 - I1_0) / (y1 -
y0);
);

The main difference with the parabola PM synth is that the I0, I1 and I2 functions use tables instead
of an integrated parabolic function.

function PIO_update2()
instance(p x1 x2 I2_1 I2_2 y0 y1 I1_0 I1_1 out)(
I2_2 = this.PIT_I2(x2);
I1_1 = x1 == x2 ? this.PIT_I1(y1) : (I2_2 - this.PIT_I2(x1)) / (x2 - x1);
);

This function must be called if the table is updated to prevent glitches.

// _____________________________________________________________________
pit.PIT_init(256);
i=0;loop(pit.n,
x = i < pit.n / 2 ? i : i - pit.n;
x *= 0.2;
x > 0 ? pit.v[i] = x / ((1+abs(x)^2.5)^(1/2.5))
: pit.v[i] = x / ((1+abs(x)^4.0)^(1/4.0));
i+=1;
);

This is the initialisaton code. It initializes the table with the dissymetric and smooth saturation.

pit.PIT_update();
pio.PIO_link(pit);
pio.PIO_update2();

updates and prepare the table.


@sample
gDriveSmooth += 0.01 * (gDrive - gDriveSmooth);
x = gDriveSmooth * spl0;
x = max(-125, min(125,x));
interp.int2_3(x + pit.n16-0.03717);
spl0 = dec2_96_59(
pio.PIO_aProc2(interp.y0),
pio.PIO_aProc2(interp.y1)
);

30
AA of Oscillators and Distortions with Pre Integrated WaveTables T.Rochebois

The sample section is x2 oversampled.

So, it uses an interpolator interp.int2_3(x + pit.n16-0.03717);

Notes:
+ pit.n16 avoids negative indexing of the table
-0.03717 offsets the input so that a zero input implies a zero output

Both interpolator outputs interp.y0 and interp.y1 are sent to the core processing
function pio.PIO_aProc2.
The results are decimated with dec2_96_59.

Oversampling is a good complement to the integration/differentiation scheme.


The integration/differentiation scheme does not suppress aliasing but it
drastically accentuate its decay rate.

Oversampling has two beneficial effects:


- it chops off the remaining aliasing
- it fixes the low pass filtering side effect of the box filtering.

31

You might also like