There are some effects that are simpler than other. Digital ones are generally easier than analog ones, and purely digital filter are also easier than digitally-transformed analog ones. Linear filters such as passband, cutband, … are easy to digitally design, chorus can be achieved through some spectral computations, delay and reverbation are computationnally expensive but easy to code.
It said that analog devices have a unique sound that digital devices cannot achieve. In fact, much is due to the simplications that occur when digitizing an analog device. One of the most blatant examples is the overdrive, which I took from Simulanalog.
A simple overdrive
I’ve started with the example in the article “State variable changes to avoid non-computational issues”. The circuit consists of a resitor, a capacitor and two diodes:

Instead of simply using a three-value state equation for the diodes, I used the full diode equation (see the equation on Wikipedia). The issue is always to pick the correct values for the reverse bias saturation current and the ideality factor. For this case, I’ll use a value of 1pA for the saturation current and of 1 for the ideality factor.
Numerical simulation
I’ve followed Simulanalog algorithm to solve the equations. The optimization will thus be done with a Newton-Raphson algorithm.
def optimize(fun, x0, x1, y0): y1 = y0 yk = y1 - fun(x0, x1, y0, y1) / fun.gradient(x0, x1, y0, y1) while(abs(y1 - yk) > 0.00001): y1 = yk yk = y1 - fun(x0, x1, y0, y1) / fun.gradient(x0, x1, y0, y1) return y1
fun is an object that will compute the value of the equation as well as its gradient. It will also call the transfer function of the diodes as well as its gradient.
class Function(object): def __init__(self, dt, R, C): self.A = dt / (2 * C) + R self.B = dt / (2 * C) - R def f(self, x): return np.sign(x) * 1e-12 * (np.exp(np.abs(x) / 26e-3) - 1) def fprime(self, x): return 1e-12 * np.exp(np.abs(x) / 26e-3) / 26e-3 def __call__(self, x0, x1, y0, y1): return self.f(y1) + 1/self.A * (y1 + (x0 - x1 + self.B * self.f(y0) - y0)) def gradient(self, x0, x1, y0, y1): return self.fprime(y1) + 1/self.A
Results
I’ve injected a 200Hz sine wave sampled at 44.1kHz inside this overdrive. It is always better to oversample your signal, especially if it is “high-frequency” (high meaning near 20kHz) to avoid audio aliasing. In this case, it is low-frequency enough so that I don’t have to use a higher sampling frequency.
So this is the original and the overdriven signals:

As you may see, there is 90° degrees phase rotation. Also, the distorted signal is not symmetric, but it’s mainly a phase issue.
This is the spectral content of the original signal:

This is the spectral content of the overdriven signal:

The spectral content of the overdriven signal is clearly odd-frequency based. The ratio for the odd or cross frequencies is very low, more complex overdrive must be created to have the “warmth” of some overdrive.
Conclusion
This simple overdrive is simple to create but shows that digitalized analog circuits are not that easy to code. Of course, it gives a better filtered signal, compared to a basic approximation of the diode. Unfortunately, a more complex overdrive will be much more complicated to program…
Here is a direct link to the Python script.
Do you know of any C libraries for fixed / random effects (multilevel) statistical analysis, other than lme4?
Thank you for the nice article! One hint: in the optimization loop you want to get f(y1)=0. Therefore, in the optimization loop, you should also check, if f is zero (or near to zero). This could allow you to converge faster and thus have a faster algorithm.
Could you post ODE for this circuit? I have problems in reverse engineering this from the code :/
I would be so grateful and donation-full 😉
The actual ODE can be found in the simulanalog paper: http://simulanalog.org/statevariable.pdf