Tanh with steepness and range parameters

I would like to use a modified version of the hyperbolic tangent function with steepness and range parameters. I usually use it like a limiter/compressor in feedback circuits.

(
~tanh2 = {arg sig, steepness, range; 
	var numerator, denominator, exponent;
	exponent = (steepness*sig + log(range)) * -1;
	numerator = 1 - (range * exp(exponent));
	denominator = 1/range + exp(exponent);
	numerator/denominator;
}
)


~tanh2.((-1,-0.99..1), 4, 1).plot
~tanh2.((-1,-0.99..1), 4, 0.5).plot
~tanh2.((-1,-0.99..1), 8, 0.5).plot

What would be the best way to implement it in SuperCollider? I would like to plug-in other audio or control rate Ugens in order to control the steepness and range parameters. Would it make more sense to create a new Ugen or a method?

1 Like

You could just put your signal through the function:

{~tanh2.(SinOsc.ar(100), MouseX.kr(4,8), 1)*0.1}.scope

Shaper could also work for a different situation:

b = Buffer.alloc(s, 1024, 1);
t = ~tanh2.((-1,-0.99609375..1), 4, 1).as(Signal);

b.sendCollection(t.asWavetableNoWrap);

{Shaper.ar(b, SinOsc.ar(200, 5),0.1)}.scope;

{Shaper.ar(b, SinOsc.ar(200, 5, Line.kr(0, 1, 20)),0.1)}.scope;

c = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav";)

{Shaper.ar(b, PlayBuf.ar(1, c, loop:1),0.1)}.scope;

Sam

Also a class would work well here!

KGTanh2 {
	*ar { |sig, steepness, range| ^this.prUgen(sig, steepness, range) }
	*kr { |sig, steepness, range| ^this.prUgen(sig, steepness, range) }

	*prUgen { |sig, steepness, range|
		var exponent = (steepness * sig) + log(range);
		var exp_expo = exp(exponent.neg);
		var numerator = 1 - (range * exp_expo);
		var denominator = range.reciprocal + exp_expo;
		^numerator / denominator
	}
}
1 Like

That’s fantastic, thanks!

I changed the formula a little bit so that it is closer to the original tanh

Tanh {
	*ar { |sig, steepness=1, range=1| ^this.prUgen(sig, steepness, range) }
	*kr { |sig, steepness=1, range=1| ^this.prUgen(sig, steepness, range) }

	*prUgen { |sig, steepness, range|
		var exponent = steepness*2*sig + log(range);
		var exp_expo = exp(exponent);
		var numerator = (range * exp_expo) -1;
		var denominator = range.reciprocal + exp_expo;
		^numerator / denominator
	}
}

With steepness=1 and range=1, it is exactly the tanh function. Would it be possible to include this in the standard sclang in the next sc version? I am mainly interested in using it for SCTwits so that everyone can play them without any externals.

General, as far as I understand, things won’t be added to the language anymore. The problem is how does a small team maintain everybody’s different version tanh (for example). But you can make a quark and upload that!

That’s totally understandable. I think though that the formula for the tanh I am proposing, has some generality in it with potential wider applications. If you want a squeezable tanh, both horizontal and vertical, this is the formula.

Well you could always move over to GitHub and have a look at how to make a contribution, or raise an issue and talk to someone on there directly.

Why not just scale the input, then scale back to the desired range, wont that have the same effect? One negative of your approach is that it uses log and exp, these are quite slow functions compared to add and mul.

Thanks for the advice and my apologies for dragging the discussion.

In non-linear circuits, tanh, as well as other functions and limiters/compressors, guarantee the stability of the system. The ability to manipulate some parameters of the transfer function opens the possibility of exploring unpredicted sonic territories.

The steepness parameter is just a multiplication. In order to keep the tanh centered in the 0 you have to use all those logarithms.

1 Like

In non-linear circuits, tanh, as well as other functions and limiters/compressors, guarantee the stability of the system.

Out of curiosity, is there a “family” of related functions that have a common use? A category of math functions would make for a useful Quark, as opposed to a one-off.

It wouldn’t hurt to submit a PR or start a discussion as an Issue on GH about including it in the class library, or even as primitives. As @jordan points out, maintenance is a consideration, but if there is a reference implementation, with bounds defined, terminology worked out, etc. I would imagine there’s support for such readily useable, parametric operations. (and everyone likes distortion operators :slight_smile:)

Thanks for the reply!

There is the family of activation functions in neural networks, from which I have drawn some inspiration. Most of them aren’t supposed to work for AC signals although some are.

I guess that only those which are symmetrical at 0 would be applicable, otherwise they will introduce some DC. Perhaps a combination of an asymmetrical function piped to a frequency-domain transfer function (such as filters that cut any DC and low frequencies), could yield interesting results.

This is another function I have been using:

Clip2 {
	*ar { |sig, steepness=1, range=1| ^this.prUgen(sig, steepness, range) }
	*kr { |sig, steepness=1, range=1| ^this.prUgen(sig, steepness, range) }

	*prUgen { |sig, steepness, range|
		var scaled = sig*steepness;
		var clipped = scaled.max(range.neg).min(range);
		^clipped
	}
}