DoubleSoftClipper

hey, ive started to rewrite this python code of the DoubleSoftClipper by Jatin Chowdhury in SC. For this specific set of argument values it looks correct on the plot already. You have this elif statement in the original code. would it be correct to just use two if statements in a row, like i have done here or is there a better way to write this?

(
var doubleSoftClipper;

doubleSoftClipper = { |x, upperLim, lowerLim, slope, xOffFactor, upperSkew, lowerSkew|
	var result, xOff;

	xOff = (1/slope) * (slope ** xOffFactor);

	result = if( x > 0, {

		x = (x - xOff) * upperSkew;

		if( x >= (1/slope), {
			upperLim
		}, {
			3/2 * upperLim * ((slope * x) - ((slope * x) ** 3 / 3)) / 2 + (upperLim/2)
		});

		if( x <= (1.neg/slope), {
			0
		}, {
			3/2 * upperLim * ((slope * x) - ((slope * x) ** 3 / 3)) / 2 + (upperLim/2)
		});

	}, {

		x = (x + xOff) * lowerSkew;

		if( x >= (1/slope), {
			0
		}, {
			3/2 * lowerLim.neg * ((slope * x) - ((slope * x) ** 3 / 3)) / 2 + (lowerLim/2)
		});

		if( x <= (1.neg/slope), {
			lowerLim
		}, {
			3/2 * lowerLim.neg * ((slope * x) - ((slope * x) ** 3 / 3)) / 2 + (lowerLim/2)
		});

	});

	result;
};

{ |x| doubleSoftClipper.(x, 1, -1, 1, 1, 1, 1) }.plotGraph(1000, -2.0, 2.0);
)

Nope, your first if does nothing since there is no return statement in suprecollider. You need to rethink this and rewrite it with a case statement, particular since two of the branches do the same thing.

var doubleSoftClipper = { |x, upperLim, lowerLim, slope, xOffFactor, upperSkew, lowerSkew|
	var	xOff = (1/slope) * (slope ** xOffFactor);
	// this is returned
	if( x > 0,  {
		var nx = (x - xOff) * upperSkew;
		var doesNothing = if( nx >= (1/slope), 
			{ upperLim }, 
			{ 3/2 * upperLim * ((slope * nx) - ((slope * nx) ** 3 / 3)) / 2 + (upperLim/2) }
		);
		// this is returned
		if( nx <= (1.neg/slope), 
			{ 0  },
			{ 3/2 * upperLim * ((slope * nx) - ((slope * nx) ** 3 / 3)) / 2 + (upperLim/2) }
		);
	}, {
		var nx =  (x + xOff) * lowerSkew;
		var doesNothing = if( nx >= (1/slope), 
			{ 0 }, 
			{ 3/2 * lowerLim.neg * ((slope * nx) - ((slope * nx) ** 3 / 3)) / 2 + (lowerLim/2) }
		);
		// this is returned
		if( nx <= (1.neg/slope),
			{ lowerLim }, 
			{ 3/2 * lowerLim.neg * ((slope * nx) - ((slope * nx) ** 3 / 3)) / 2 + (lowerLim/2) }
		)
	})
};

This is a better way to write it. The python code you were looking at was also poorly written, there was no need to duplicate all the equations.

(
var softclipper = { |x, upperLim, lowerLim, slope, xOffFactor, upperSkew, lowerSkew|	
	var softclippermain = { |scaledIn, limit|
		var s = slope * scaledIn;
		var c = s - (s.pow(3) / 3);
		(3/2 * limit * c).half + limit.half
	};
	
	var clipper = { |in, limit, skew, floor, ceil|
		var xOff = slope.reciprocal * slope.pow(xOffFactor);
		var scaledIn = in + (xOff * in.neg.sign) * skew;
		case (
			{ scaledIn >= slope.reciprocal },     { ceil },
			{ scaledIn <= slope.reciprocal.neg }, { floor },
			/*default */ { softclippermain.(scaledIn, limit) }
		)
	};
	
	if(x > 0, 
		{            clipper.(x, upperLim, upperSkew, floor: 0, ceil: upperLim ) },
		{ lowerLim + clipper.(x, lowerLim, lowerSkew, floor: 0, ceil: lowerLim ).neg });
};

softclipper.(_, 
	upperLim: 1,  lowerLim: -1, 
	slope: 1,  xOffFactor: 20, 
	upperSkew: 1,  lowerSkew: 4
).plotGraph(1000, -2.0, 2.0);

)

okay, thank you very much.

thanks, i will have a look at this in more detail.

for beeing used in a SynthDef you then have to rewrite all the if statements including the case, or?

(
var softclipper;

softclipper = { |x, upperLim, lowerLim, slope, xOffFactor, upperSkew, lowerSkew|
	var result, softclippermain, clipper;

	softclippermain = { |scaledIn, limit|
		var s = slope * scaledIn;
		var c = s - (s.pow(3) / 3);
		(3/2 * limit * c).half + limit.half
	};

	clipper = { |in, limit, skew, floor, ceil|
		var xOff = slope.reciprocal * slope.pow(xOffFactor);
		var scaledIn = in + (xOff * in.neg.sign) * skew;
		case (
			{ scaledIn >= slope.reciprocal },     { ceil },
			{ scaledIn <= slope.reciprocal.neg }, { floor },
			/*default */ { softclippermain.(scaledIn, limit) }
		)
	};

	result = Select.ar(x > 0, [
		clipper.(x, upperLim, upperSkew, floor: 0, ceil: upperLim ),
		clipper.(x, lowerLim, lowerSkew, floor: 0, ceil: lowerLim ).neg + lowerLim
	]);

	result;
};

{
	var sig;

	sig = SinOsc.ar(440);
	sig = softclipper.(sig, 1, -1, 1, 1, 1, 1);

	sig !2 * 0.1;
}.play;
)

You are right you can’t use an if on the server, but you can’t use case statements either. Just a little thing here…

var result, softclippermain, clipper;

Try not to put all the variables at the top like this, its much better to declare them as you go down (where possible). Also, this result variable is unnecessary, functions always return the result of the last expression.

	result = Select.ar(x > 0, [
		clipper.(x, upperLim, upperSkew, floor: 0, ceil: upperLim ),
		clipper.(x, lowerLim, lowerSkew, floor: 0, ceil: lowerLim ).neg + lowerLim
	]);

	result;

It’s not a bad way of shaping a signal, but it is terribly slow for full runtime use. Here are two versions, one that is essentially a pseudo UGen and another that can be used to generate a buffer. The UGen one is a little messy, I haven’t found a nicer way of write functions that work at both control and audio rate.

(
~softclipperUgen = { 	
	|x, upperLim, lowerLim, slope, xOffFactor, upperSkew, lowerSkew|
	var rate = if(x.rate == \audio, \ar, \kr);
	var zero = DC.perform(rate, 0); // useful to force a number -> ugen
	
	// the actual softclip
	var softclippermain = { |scaledIn, limit|
		var s = slope * scaledIn;
		var c = s - (s.pow(3) / 3);
		((3/2 * limit * c)) / 2 + (limit / 2)
	};
	// if input is out of range, return floor or ceil
	var clipper = { |in, limit, skew, floor, ceil|
		var xOff = slope.reciprocal * slope.pow(xOffFactor);
		var scaledIn = in + (xOff * in.neg.sign) * skew;
		// 0 is normal, 1 is  ceil, 2 is floor -- this is a subtle line of code
		var index = 
		((scaledIn >= slope.reciprocal) * 1) + 
		((scaledIn <= slope.reciprocal.neg) * 2);
		
		Select.perform(rate, zero + index,
			[softclippermain.(scaledIn, limit), ceil, floor]
		);
	};
	
	Select.perform(rate, x > zero,  [
		clipper.(x, upperLim, upperSkew, floor: zero, ceil: zero + upperLim ) ,
		lowerLim + clipper.(x, lowerLim, lowerSkew, floor: zero, ceil: zero + lowerLim ).neg 
	] );
};

~softclipper = { |x, upperLim, lowerLim, slope, xOffFactor, upperSkew, lowerSkew|	
	var softclippermain = { |scaledIn, limit|
		var s = slope * scaledIn;
		var c = s - (s.pow(3) / 3);
		(3/2 * limit * c).half + limit.half
	};
	
	var clipper = { |in, limit, skew, floor, ceil|
		var xOff = slope.reciprocal * slope.pow(xOffFactor);
		var scaledIn = in + (xOff * in.neg.sign) * skew;
		case (
			{ scaledIn >= slope.reciprocal },     { ceil },
			{ scaledIn <= slope.reciprocal.neg }, { floor },
		    { softclippermain.(scaledIn, limit) }
		)
	};
	
	if(x > 0, 
		{            clipper.(x, upperLim, upperSkew, floor: 0, ceil: upperLim ) },
		{ lowerLim + clipper.(x, lowerLim, lowerSkew, floor: 0, ceil: lowerLim ).neg }
	);
};

)


(
// uses the function live, you can alter all the argument with ugens

s.waitForBoot {
	{  
		var a = LFSaw.ar(1); 
		[ a,  ~softclipperUgen.(a.range(-2,2),  1, -1,  1, 1,  1,  1 ) ]
	}.scope;
}

)


(
// freezes these arguments and stores them in a buffer.
s.waitForBoot {
	~b = Buffer.alloc(s, 1024);
	~t = Signal.fill(513, { |n| 
		~softclipper.( n.linlin(0, 512, -2.0, 2.0),  1, -1,  2, 1,  1,  1 )
	});
	s.sync;
	~b.sendCollection(~t.asWavetableNoWrap);
	
	// Shaper reads from the buffer
	{
		var a = LFSaw.kr(1); 
		[ a,  Shaper.kr(~b, a)]
	}.scope
}
)

waoh, thank you very much for taking so much time on this. i will look at both approaches.

In general im interested in non-linear functions which could be used as dynamic waveshapers. So probably a case where this approach has to be written in C++ and be compiled as a Ugen if you also consider the “antiderivative anti-aliasing” which has been presented in the article.

EDIT: There is a really nice series of posts here about non-linear functions used as waveshapers:

https://jatinchowdhury18.medium.com/complex-nonlinearities-episode-0-why-4ad9b3eed60f

why is it better to declare them as you go down?

In supercollider we have to declare all the variables before we use them in one continuous block due to a design decision made in the late 90s/early 2000s to make parsing quicker - super import back then, not so much now. As a result the code is harder to read and promotes more mutable state. Defining your variables as you declare them is one method to combat this.

You can actually do the ‘antiderivative’ stuff pretty simple…

Slope.ar( Shaper.(~intergratedSoftClipBuffer, x) ) / Slope.ar(x)

… You just have to compute a rolling sum of the buffer and do some checking to make sure the bottom isn’t too close to zero. The problem is that you can’t oversample in supercollider outside of C++.

You should check out something like wave terrain synthnesis instead, I think there’s something in the sc3ugens, basically you make a 3D surface that you interpolate across - like Shaper but with and X and a Y input.

I’ve often found that the type of wave shaper (within reason) has a reasonably small effect and that you get more useful results by doing emphasis - pre and post eq…

var a = BLowShelf.ar(x, 400, dB: 5);
var b = Shaper...;
var c = BLowShelf.ar(x, 400, dB: 5.neg);

thats interesting, ive picked this up while reading this thread and looking at the shared content:

thanks for the tip!
ive once done that, was pretty hard for me to digest whats actually going on. will give it another go.

One simple way to make a double-soft-clipper nonlinearity is to compose a pinch nonlinearity with a saturation nonlinearity, like (x ** 5).tanh.

If you’ll forgive me, I disagree with this actually. (And I may have been responsible for influencing @dietcv’s coding style.) If you do variable declarations and assignments at the same time, then it forces a refactor if you need to insert something before a particular assignment. For example:

var x = doThing1.();
var y = doThing2.();
doOtherThing.();

What if I need to move doOtherThing.() so that it’s between doThing1.() and doThing2.()? I always declare all my vars at the top so I never have to think about this.

Sure, sometimes it’s necessary to pre declare… but the main issue with the code posted was that an ‘if’ statement was free floating and didn’t return to anything. The problem with pre-declaring is that it encourages bad function writing where the results are not stored or used, ultimately it promotes a misunderstanding around ‘everything returns’.

The code you have posted, I think is actually wrong, doThing1 is the wrong way to think about functions. Functions do not do stuff, they calculate and return - sure there are exceptions to this but by sticking to pure functions, where easy to do so, a lot of programming pit falls can be avoided. For example, the aforementioned if statement above is an expression, but its returned value is ignored, this isn’t possible with a more functional approach, the misunderstanding simply doesn’t arrise.

Also, it’s worth mentioning that mainstream program languages stop pre declaring their variables this way like 30 years ago, and I think it’s a good idea to make supercollider as pedagogy transferable as possible - and as more functional inspired styles are on the rise why not jump on the wagon?.

There’s a bunch of stackoverflow debates on this topic in relation to C90 if you want more nuanced opinions.

It would be very nice if someone looked at the parser and made this happen.

Also, whenever the subject comes up, I have to mention this cute edge case:

(
var y = x.rand;
var x = 10;  // thank you, prototypeFrame ;-)

[x, y]
)

hjh

What some call cute, others call evil magic …

It’s not really useful for anything because it only works for literals; I don’t think anyone would deliberately write like this and rely on it.

hjh

To actually answer the question…

There is only one reason to have a function call whose return value is unimportant, that is I/O (or async processing but these can usually be broken down into separate functions).

Its also worth just reiterating this thread is more about how these two lines got written, how to avoid writing them, why they are wrong, but also (and I think more importantly), how to alter ones teaching (and the supercollider pedagogy as a whole) so that these mistakes do not arise, how supercollider should be considered not just as a language for making art where things are ‘hacked’ together, but as a good first language that engenders a sustainable coding practice from which a student may launch into other languages and the wider technological landscape.

the code in question
		if( x >= (1/slope), { // this line does nothing as the returned value is not used
			0
		}, {
			3/2 * lowerLim.neg * ((slope * x) - ((slope * x) ** 3 / 3)) / 2 + (lowerLim/2)
		});

		if( x <= (1.neg/slope), {
			lowerLim
		}, {
			3/2 * lowerLim.neg * ((slope * x) - ((slope * x) ** 3 / 3)) / 2 + (lowerLim/2)
		});

If output

If you make sure all the variables are immutable then it doesn’t matter where in the function scope it goes (assuming your not in a routine or some sort of async context), therefore, this is always valid.

var x = getThing1.();
var y = getThing2.();
//....
outputSomething.(x);

or to put it another way, as long as my_amazing_sound is immutable then the Out call can go anywhere after it, i.e., right at the bottom. The exception here is if you are reading and writing to the same buffer in a function.

{
  var my_amazing_sound = ..;
  ... several lines of code
  Out.ar(0,  my_amazing_sound);
}

If input

If you have to set some state to get an input, then this should go inside the getter function.

var getThing2ThatRequiresSettingSomthing = { |arg|
   prepareToGetInputFromSomewhere(arg);
   actuallyGetOut();
};

var x = getThing1.();
var y = getThing2ThatRequiresSettingSomthing.();

Now the contents of getThing2ThatRequiresSettingSomthing might have mutable variables in, or have to pre-declare some things, which is fine, because its unavoidable.The point is to limit these as edge cases, and more importantly in the context of this thread, to teach them as the exception and a part of the history of supercollider - much like how modern C++ is very different from older C++ and how the language has evolved (have a look at Kate Gregory’s talks on C++, they are really amazing - CppCon 2017: Kate Gregory “10 Core Guidelines You Need to Start Using Now” - YouTube - this one has a slide called ‘Don’t run with scissors’ and is about dealing with legacy styles).

Other ways.

All things return

Since all lines return, this is completely valid … if a little odd…

var x = doThing1.();
var unused_var_1 = doOtherThing.();
var y = doThing2.();

I end up doing this when using BufWr and BufRd together (when targeting the same buffer).

Immediately invoked functions

Overkill it the situation you proposed.

var x = {
   var out = getThing1.();
   doSomethingWith(out);
   out
}.();
var y = doThing2.();

But this one is actually really useful in audio processing when you have several different sound streams and have lots of variables that you might get confused about…

var grains = {
   var raw = GrainBuf( ... );
   var lpf = LPF.ar(raw);
   var hpf ...
   var bpf ...
   ...returning expression here
}.();
var wash = {
   var raw = WashyUGenf( ... );
   var lpf = LPF.ar(raw);
   var hpf ...
   var bpf ...
   ...returning expression here
}.();

The benefit of this is that is very easy to see where all the grains' processing takes place and where all of wash's processing takes place and its very difficult to get them confused. Also if you use an IDE with code folding its even easier.

I sadly can’t spare the time to address all your arguments. I hold that a function can change state and return an error code, even if it isn’t common. Back when I used var x = ... I ran into other cases that required refactoring and didn’t involve such functions, but I can’t remember what they were now.

I will also throw another example into the mix, probably related to James’:

(
var x = 3;
{
	var y = x;
	var x = 4;
	y;
}.();
)
// -> 4

Also, apologies to dietcv for hijacking this thread. Maybe this discussion can be broken off into a new thread.