[Sharing is Approximating Tanh]

Cptnfantasy's icon

I absent-mindedly googled Tanh Approximation and wound up with this two hours later. Its a colection of 20 or so Tanh approx algos in genexpr that you can compare side by side. There are some more I want to add, but its getting late and I need to do the dishes right now. Maybe I will update later. Links are provide to the threads/sites/blogs ect.

Anyway, hopefully someone else will find this useful too.

Max Patch
Copy patch and select New From Clipboard in Max.

.quasar's icon

Oh nice ! Thanks for this! I was actually thinking about tanh saturation for virtual analog filters the other day so all this will soon be quite helpful I believe

Ernest's icon

That's some good work, but I actually have to side with Peter Mucullogh on this, please forgive me if I dont spell his name correctly.

The best solution is to precalculate the exact values and put them in a buffer~. Then you only need a peek~ to determine the value. A resolution of 16.384 values is sufficient. Moreover, what my Synthcore library does is combine all the coefficients into the buffer~, so there's only one lookup and a multiply for the EPTR oscillators. You can find an illustration of the technique here:

https://www.yofiel.com/software/cycling-74-patches/antialiased-oscillators

Cptnfantasy's icon

Here is an update with an additional ten or so approximations. Its still kinda disorganized, sorry.
I think the best ones (read: most similar to tanh) are the lambert aprox, the rational tanh (from musicdsp.org), and the two sqrt approximations. The other best ones are the ones that create weird non-linearities =p

@Ernest Its funny, I had originally also made the assumption that using a lookup table and buffer would be hands down faster than any approximation, but I added an implementation (near the top right of the patch) and was surprised to see that it is kind of a cpu hog compared to the others. This is of course somewhat dissimilar to your osc algos because I'm using [lookup] to account for an arbitrary frequency. Maybe there is a faster way?

Interestingly, I implemented (exp(2*x) - 1) / (exp(2*x) + 1) which is how I would think the native gen function would do tanh, but it is a super cpu hog compared to the native function. Maybe gen tanh is a bit of an approximation too?

Max Patch
Copy patch and select New From Clipboard in Max.

Ernest's icon

Thanks for your answer. In gen~ the lookup() and sample () functions are not very efficient. Well properly speaking anything that sets attributes to the same as the defaults for lookup() and sample(). The explanation is that these objects interpolate in a fixed range, such as 0 to 1, to find the corresponding value in the LUT. This means it performs a divide by the buffer size, and it also has to fetch the buffer size from patch data. If you use peek~ and efficiently precalculate the range yourself, as you already know what size table you chose, the performance is closer to what one expects.

There's still a little overhead because if you use an external buffer~ to store the data, the buffer~ object is multiclient. That means the access to gen~ is double buffered, which slows it down. So the most efficient implementation is to calculate the values inside gen~ in a Data() array, rather than going to an external buffer~.

Cptnfantasy's icon

Very interesting, thanks! I am going to set to work on this and see if I can come up with a efficient solution using [data] and [peek].

Ernest's icon

Oh, well if you want to do it, then the best solution currently for setting data currently, as per kind directions from cycling74:

Data mydata(16384);
History flag(0);
if (flag==0){
flag=1;
for (x=0; x<163784; x +=1) {
// something like mydata.poke(tanh (x/16384), x);
}

So it still needs an additional conditional test and static variable read on every sample cycle, which may be annoying but there apparently isn't a better way known to populate read-only data arrays in gen~ currently. Hopefully it combines with setting other static data in a real-world design.

Ernest's icon

Funnily though I may be goling in the other direction and wanting your approximations, because in my poly~ synth, all the data buffering has made then gen~ code too large to run on 32-bit Windows. Whenever you start doing alot of data in gen~ it's a good idea to check the memory usage too.

Cptnfantasy's icon

Hey, sorry for the delayed response, more pressing issues placed my late-night tanh tomfoolery in the low priority queue for a bit but last night I had enough juice left in me to do some stuff that had been on my mind.

And btw, Wow! Ernest, that history-branch-loop trick is amazing, thanks! Its like a loadbang for genexpr! I have a feeling its going to be useful in many others ways too...

So, I rigged up an uninterpolated lookup transfer function and it does do better than [lookup] or [sample @index="lookup"] in terms of CPU but it is still outperformed by the better approximations. Outperformed in terms of CPU usage and responsiveness to high gain levels. Maybe it could be implemented better than I did it? I am not really privy to the best practices in terms of LUT transfer function code, I just did it the easiest way that made sense to me. Honestly this result surprises me about LUTs being harder on the CPU, but hey, what do I know?

I also started thinking about using branching as well as exp() approximations to get a tanh() approx and I sort of had some success. I included two of my most successful experiments near the bottom right of the patcher: one is a branched tanh-ish thing that is a little too bright and has a variable shape built it. Its cheap and could be useful for alot of stuff. The other is calculated tanh() with fast exp() approximations that isn't great, but might be viable with some fixing up. I think I'll keep poking around other fastexp() type deals they seem pretty useful. Also, I was thinking about a taylor series type implementation being viable? But the higher order coefficients require something like 60x^9 or something ludicrous, so maybe not.

anyway with further ado...

Max Patch
Copy patch and select New From Clipboard in Max.


Cptnfantasy's icon

Lol, I just realized I left a comment with a typo in there that says "[lookup] peeling a buffer".