Share your waveshapers!

Hello everyone
I have recently been inspired to use the wonderful Shaper UGen a bit more and I am curious to see/hear other people’s waveshaping signals to be used with Shaper. Link to helpfile.

Here is a little something I currently use in my library. I ripped out the waveshaping buffers from Aaron Lanterman’s buchla700 clone and created my own variant of them and wrote some wrapper functions that can easily be used in SynthDef production (see the comments below for examples).

// Waveshaping buffers to be used with Shaper.ar
// Some of these were pulled out of Aaron Lanterman's Buchla 700 project: https://github.com/lantertronics/b700ish/blob/main/b700ish.scd
/*
(

Ndef(\lfo, {|freq=0.1| LFTri.kr(freq) });

Ndef(\a, {|freq=141, modfreq=0.1, shapebufA, shapebufB, crossfade=0, amp=0.1|
	var lfo = LFTri.kr(modfreq);
	var sig = SinOsc.ar(freq: SinOsc.ar(lfo.lag.lag*215) * lfo.lag.exprange(freq/2.0, freq),  phase: 0.0,  mul: 1.0,  add: 0.0);
	var shapeA = Shaper.ar(shapebufA, sig);
	var shapeB = Shaper.ar(shapebufB, sig);

	sig = XFade2.ar(shapeA, shapeB, crossfade) * amp;
	Pan2.ar(sig * lfo, lfo);
})
.set(
	\shapebufA, ~shaperBuffers[\fulltonewheel], 
	\shapebufB, ~shaperBuffers[\altsaw]
)
.map(\crossfade, Ndef(\lfo))
.mold(2)
.play;

Ndef(\a).copy(\b).set(
	\freq, 182,
	\modfreq, 0.185,
	\shapebufA, ~shaperBuffers[\truesquare], 
	\shapebufB, ~shaperBuffers[\altsignflipping_impulse_train]
).play;

Ndef(\b).copy(\c).set(
	\freq, 582,
	\modfreq, 5.085,
	\shapebufA, ~shaperBuffers[\jimmysmith], 
	\shapebufB, ~shaperBuffers[\truetriangle]
).play;
)

*/
(
s.waitForBoot{
	"Loading waveshaper algos".postln;

	// Coeffecients
	~waveShapeCoeffecients = IdentityDictionary.new;
	~waveShapeCoeffecients.put('truetriangle ', [1,0] / ((1..32).squared));
	~waveShapeCoeffecients.put('squarecompatible_triangle', [1,0,-1,0] / ((1..32).squared));
	~waveShapeCoeffecients.put('jimmysmithpositive', [1,1,1]);
	~waveShapeCoeffecients.put('jimmyshith', [1,1,-1]);
	~waveShapeCoeffecients.put('fulltonewheel', [1,1,-1,-1,0,1,0,-1,0,1,0,-1,0,0,0,1]);
	~waveShapeCoeffecients.put('truesquare', [1,0,-1,0] / (1..32));
	~waveShapeCoeffecients.put('trianglecompatible_square', [1,0] / (1..32));
	~waveShapeCoeffecients.put('altsaw', 0.25*[1,-1,-1,1] / (1..32));
	~waveShapeCoeffecients.put('altimpulsetrain', 0.1*[1,-1,-1,1]*Array.fill(32,1));
	~waveShapeCoeffecients.put('altsignflipping_impulse_train', 0.1*[1,0,-1,0]*Array.fill(32,1));

	// Convert coeffecients to waveshaping buffers
	~shaperBuffers = IdentityDictionary.new;
	~waveShapeCoeffecients.keysValuesDo{|name, coeffecients|
		var waveShapeSignal = Signal.chebyFill(4096, coeffecients, normalize: true, zeroOffset:false);
		var buf = Buffer.loadCollection(s, waveShapeSignal.asWavetableNoWrap);
		~shaperBuffers.put(name, buf);
	};

	"Waveshaping buffers are available in ~shaperBuffers using the following keys:".postln;
	~shaperBuffers.keys.do{|k| k.postln};

	// A function for embedding in a synthdef using SynthDef.wrap
	/*

	SynthDef.new(\lol, {|out=0, amp=0.25, dur=1|
		var sig = SinOsc.ar(141);
		sig = SynthDef.wrap(~waveshapeXWrap,  prependArgs: [sig]);

		Out.ar(out, Env.perc.kr(gate:1, timeScale: dur) * sig * amp)
	}).add;

	Synth(\lol, [
		\waveshapeCrossfade, 0.5, 
		\waveshapeA, 0,
		\waveshapeB, 3,
	]);

	*/

	// This one crossfades
	~waveshapeWrap = {
		arg sig, waveshapeAmount=1, waveshape=0;
		var clean = sig;
		var shapeBuffers = SynthDef.wrap({ ~shaperBuffers.asArray });
		var shape = Select.kr((waveshape % ~shaperBuffers.size), shapeBuffers);
		sig = Shaper.ar(shape, sig);
		XFade2.ar(clean, sig, waveshapeAmount.linlin(0.0,1.0,-1.0,1.0));
	};

	// This one crossfades
	~waveshapeXWrap = {
		arg sig, 
		waveshapeAmount=1,
		waveshapeA=0,
		waveshapeB=4,
		waveshapeCrossfade=0;
		var clean = sig;

		var shapeBuffers = SynthDef.wrap({ ~shaperBuffers.asArray });
		var shapeA = Select.kr((waveshapeA % ~shaperBuffers.size), shapeBuffers);
		var shapeB = Select.kr((waveshapeB % ~shaperBuffers.size), shapeBuffers);

		var sigA = Shaper.ar(shapeA, sig);
		var sigB = Shaper.ar(shapeB, sig);

		sig = XFade2.ar(sigA, sigB, waveshapeCrossfade);

		XFade2.ar(clean, sig, waveshapeAmount.linlin(0.0,1.0,-1.0,1.0));
	};


}
)
7 Likes

Hi,

.) In general I think it’s more comfortable to use BufRd for waveshaping than Shaper for two reasons:
(a) BufRd avoids the need to fiddle around with the wavetable format.
(b) BufRd allows to choose different interpolation modes including cubic.
There might be performance advantages when using a lot of shapers, though I never encountered a situation where this played a role.

.) I find it interesting to take arbitrary waveforms, including recorded sounds, as a transfer function. I’ve included some examples for this in my last year’s TU Berlin workshop:
https://daniel-mayer.at/materials/Berlin_2020/UOS_2020_TUBerlin.zip
UOS_02_buffer_modulation.scd

.) Alternatively you can define a dynamic waveshaper by a combo of mathematical operators. As an advantage this allows more modulation options as parameters of the shaper can be controlled by signals. E.g. a parametric saturation function by Partice Tarrabia and Bram de Jong
https://www.musicdsp.org/en/latest/Effects/42-soft-saturation.html

// amount must be >= -1 and < 1 !


(
f = { |x, amount = 0|
	var k = 2 * amount / (1 - amount);
	(1 + k) * x / (1 + (k * x.abs));
};

{ |x| f.(x, 0.5) }.plotGraph(500, -1, 1);
)


{ |x| f.(x, -0.9) }.plotGraph(500, -1, 1);

{ |x| f.(x, 0) }.plotGraph(500, -1, 1);

{ |x| f.(x, 0.9) }.plotGraph(500, -1, 1);


(
SynthDef(\flexShape, { |out = 0, in, mix = 1, amount = 0, amp = 1|
	var sig = In.ar(in, 2), k = 2 * amount / (1 - amount);
	sig = (1 + k) * sig / (1 + (k * sig.abs));
	XOut.ar(out, mix, sig * amp);
}).add;
)

// start silently
y = Synth(\flexShape);


// default amount = 0, no change
x = { SinOsc.ar(300 + [0, 0.1], 0, 0.2) }.play

y.set(\amount, 0.5, \amp, 0.3)

y.set(\amount, 0.9, \amp, 0.1)

x.release;
y.free
6 Likes

This is a very elegant solution Daniel. There is a whole world here hidden in waveshaping! Do you have any advice in terms of avoiding aliasing?

I have no general approach. With noisy textures I simply don’t mind. If you have a sine source only – because, obviously, you can’t easily filter away the aliasing resulting from waveshaping – you’d have to tweak the waveshaping itself, maybe depending on the sine frequency.
With a source different from a sine though, you can filter, so at least preventing higher partials from generating aliasing e.g.

// start silently
// ATTENTION: with large amount you should compensate amp
y = Synth(\flexShape, [amount: 0.995, amp: 0.03]);


// reduce aliasing with low cutoff
(
x = { 
	var src = VarSaw.ar(1000 + [0, 5], 0, 0.1, 0.1);
	LPF.ar(src, MouseX.kr(300, 5000));
	// src
}.play
)

x.release;
y.free;   

BTW at the Heretical Sound Synthesis Symposium 2019 at Sibelius Academy Helsinki I spoke about some buffer modulation variants of waveshaping. The video, as all lectures of this symposium, are on YouTube

4 Likes

Is this the proper way to switch from Shaper to BufRd ?

(
// Usage with Shaper
~tf = Env([-0.8,0,0.8], [1,1], [8,-8]).asSignal(1025);
~tf = ~tf.asWavetableNoWrap;
~tfBuf = Buffer.loadCollection(s, ~tf);
)

{Shaper.ar(~tfBuf, SinOsc.ar()) * 0.1 !2}.play;


(
// Usage with BufRd
~tf = Env([-0.8,0,0.8], [1,1], [8,-8]).asSignal(1024);
~tfBufrd = Buffer.loadCollection(s, ~tf);
)

{BufRd.ar(1,~tfBufrd, SinOsc.ar().range(0,BufFrames.ir(~tfBufrd))) * 0.1}.play;

{BufRd.ar(1,~tfBufrd, LinLin.ar(SinOsc.ar(), -1.0, 1.0, 0.0, BufFrames.ir(~tfBufrd.bufnum))) * 0.1}.play;

In the last two I am getting a fast gap in the waveform… Is it possible to be a bug with Env().asSignal() ?

No, it’s just the last index is BufFrames.ir(...) - 1

{ 
	BufRd.ar(
		1,
		~tfBufrd, SinOsc.ar().range(0, BufFrames.ir(~tfBufrd) - 1)
	) * 0.1	
}.play;
1 Like