Phaseshaping Osc Algorithms

hey, ive come across this paper presenting Phaseshaping Osc Algorithms and was wondering if someone could guide me in the right direction for implementing for example the 3.5 “Triangle Modulation” and 3.7 “Phaseshaping for a Sinusoidal Waveshaper” as presented in the paper in supercollider. thanks alot :slight_smile:

Unless I am reading this wrong, the Triangle Modulation is just a folded triangle wave:

{LFTri.ar(200, 0, LFTri.ar(0.2).range(1,2)).fold(-1,1)*0.1}.plot;
{LFTri.ar(200, 0, LFTri.ar(0.2).range(1,2)).fold(-1,1)*0.1}.play;

Somebody has probably made a better SuperSaw, but here is a 7 saw version:

{Mix(LFSaw.ar((200+LFNoise2.kr(Rand(0.1,0.2)!7, 2)).poll, 0, 0.1/8)).dup}.play

This is not something I really use, so I am just going on the description.

The last one is the trickiest:

{
	var saw = LFSaw.ar(400, 1);
	var trig = ToggleFF.ar(Trig.ar(saw.neg, 2/SampleRate.ir));
	a = saw.range(0,2pi).sin;
	b = saw.range(0,pi).sin;
	Select.ar(trig, [a,b])
}.plot;

{
	var saw = LFSaw.ar(200, 1);
	var trig = ToggleFF.ar(Trig.ar(saw.neg, 2/SampleRate.ir));
	a = saw.range(0,2pi).sin;
	b = saw.range(0,pi).sin;
	Select.ar(trig, [a,b]).dup*0.2
}.play;

That one took some thought.

edit: now that I look at this, I didn’t implement the variable width. That is more challenging.

Sam

2 Likes

thanks for your help, i will have a look later on. Im mostly interested in the “Phaseshaping for a Sinusoidal Waveshaper” :slight_smile:

for the supersaw ive already found a nice implementation of the Roland JP-8000 detune characteristics

1 Like

Is this it:

({
	var width = MouseX.kr().clip(0.05,0.95);
	var freq = MouseY.kr(200, 1000);
	var saw = LFSaw.ar(freq/2, 1).linlin(-1,1, width.neg, 1-width);
	var trig = ToggleFF.ar(Trig.ar(saw.neg, 2/SampleRate.ir)+Trig.ar(saw, 2/SampleRate.ir));
	a = saw.linlin(width.neg, 0, 0, pi).sin;
	b = saw.linlin(0, 1-width, 0, 2pi).sin;
	
	Select.ar(trig, [b,a]).dup*0.2
}.scope;)
1 Like

thanks so much :slight_smile: its sounding really nice. should the input always be a Saw? I was wondering if its also possible to use BufRd for the input with some One Cycle Waveforms.
i also recognized that the freq is divided by 2 so its always transposed down an octave.

You can use the Shaper, but my sense is the algorithm would basically be the same. The variable width thing makes it tricky. You would still need two Shapers and would need to toggle between them. At least I think so. I’m glad to be wrong.

As for the Saw, look at the paper. There is a reason they are drawing sawtooth waves all over the diagrams. It is just a ramp, and you can use it to the make all the other oscillators really easily:

{LFSaw.ar(200)>0}.plot;  //square
{LFSaw.ar(200).abs}.plot  //tri
{LFSaw.ar(200, 1, pi, pi).sin}.plot  //sine

But I thought about this some more and came up with a much cleaner solution (that ToggleFF was bugging me):

({
	var width = 0.25;
	var freq = 400;
	var saw = LFSaw.ar(freq/2, 1).linlin(-1,1, width.neg, 1-width);
	var outsaw, output;
	
	saw = saw.bilin(0, width.neg, 1-width, 0, -0.5, 1);

	outsaw = Select.ar(saw>0, [0.5+saw, saw]);
	output = (outsaw*2pi).sin;
	
	[saw, outsaw, output]
}.plot;)

({
	var width = MouseX.kr().clip(0.001,0.999);
	var freq = MouseY.kr(200, 1000);
	var saw = LFSaw.ar(freq/2, 1).linlin(-1,1, width.neg, 1-width);
	var output;
	
	saw = saw.bilin(0, width.neg, 1-width, 0, -0.5, 1);

	output = (Select.ar(saw>0, [0.5+saw, saw])*2pi).sin;
	
	output
}.play;)

Sam

1 Like

And…thinking some more, of course you can use a Shaper with that:

b = Buffer.alloc(s, 512, 1, { |buf| buf.chebyMsg([1,0,1,1,0,1])});
({
	var width = MouseX.kr().clip(0.1,0.9);
	var freq = MouseY.kr(200, 1000);
	var saw = LFSaw.ar(freq/2, 1).linlin(-1,1, width.neg, 1-width);
	var sine, shaper;
	
	saw = saw.bilin(0, width.neg, 1-width, 0, -0.5, 1);

	sine = (Select.ar(saw>0, [0.5+saw, saw])*2pi).sin;
	
	shaper = Shaper.ar(b, sine, 0.5);
	
	shaper.dup
}.scope;)

thanks so much for your further explanations. the Shaper idea is great :slight_smile:

is it also possible to exchange LFSaw and Shaper with Phasor and BufRd. I would like to retrigger the Phasor with a multichannel envelope for granulation. thanks

(
b = Buffer.alloc(s, 512, 1, { |buf| buf.chebyMsg([1,0,1,1,0,1])});

~maxOverlap = 12;
~audioBus = Bus.audio(s, ~maxOverlap);

SynthDef(\gran_1f, {
	arg out=0, soundBuf=0, overlap=2, trigRate=1,
	panMax=0.8, amp=1, freq=150, widthMod=0.3;
	
    var sig, pos, playbuf, env, saw, sine, shaper;
	var maxOverlap = ~maxOverlap;
	var width = LFDNoise3.ar(widthMod!2).range(\widthMin.kr(0.05), \widthMax.kr(0.95).clip(0.001,0.999).lag(0.01));
	
	saw = LFSaw.ar(freq/2, 1).linlin(-1, 1, width.neg, 1-width);
	saw = saw.bilin(0, width.neg, 1-width, 0, -0.5, 1);
	sine = (Select.ar(saw>0, [0.5+saw, saw]) * 2pi).sin;
	
    // multichannel trigger
    env = DXEnvFan.ar(
        Dseq((0..maxOverlap-1), inf),
        trigRate.reciprocal,
        size: maxOverlap,
        maxWidth: maxOverlap,
        width: (Main.versionAtLeast(3, 9)).if { overlap }{ 2 },
        zeroThr: 0.002,
        equalPower: 0,
        bus: ~audioBus
    );
	
    // multichannel playback, pos is triggered for each grain
	pos = Phasor.ar(env, freq * BufFrames.ir(soundBuf) / SampleRate.ir, 0, BufFrames.ir(soundBuf));
	//shaper = Shaper.ar(soundBuf, sine);
	playbuf = BufRd.ar(1, soundBuf, pos, interpolation:4);
	
    // generate grains by multiplying with envelope
    sig = playbuf * env;

    // generate array of 12 stereo signals
    sig = Pan2.ar(sig, Demand.ar(env, 0, Dseq([-1, 1], inf) * panMax));
	
	sig = Mix(sig) * amp;
    Out.ar(out, sig)
}).add;
)

(
Pmono(\gran_1f,

	\soundBuf, b,
		
	\trigRate, 5,
	\overlap, 2,
	\panMax, 0.80,
		
	\widthMod, 15,
	\widthMin, 0.05,
	\widthMax, 0.95,

	\midinote, Pseq([
		[57,64,70,76,77]
	],inf),

	\amp, 0.04,

	\out, 0,
).play;
)

i was trying to emulate the LFSaw behaviour with Phasor. is this right? At 0.005 sec its not accurate why is that?

(
{
	var width = 0.25;
	var freq = 400;
	var saw = LFSaw.ar(freq/2, 1).linlin(-1, 1, width.neg, 1-width);
	var pos = Phasor.ar(0, freq/2 * SampleRate.ir.reciprocal, width.neg, 1-width);
	//var pos = Phasor.ar(0, freq * BufFrames.ir(b) * SampleRate.ir.reciprocal, 0, BufFrames.ir(b));
	
	saw = saw.bilin(0, width.neg, 1-width, 0, -0.5, 1);
	pos = pos.bilin(0, width.neg, 1-width, 0, -0.5, 1);
	
	[saw, pos]
}.plot;
)

It is really only off by 1 sample:

(
{
	var width = 0.25;
	var freq = 400;
	var saw = Delay1.ar(LFSaw.ar(freq/2, 1).linlin(-1, 1, width.neg, 1-width));
	var pos = Phasor.ar(0, freq/2 * SampleRate.ir.reciprocal, width.neg, 1-width);
	
	saw = saw.bilin(0, width.neg, 1-width, 0, -0.5, 1);
	pos = pos.bilin(0, width.neg, 1-width, 0, -0.5, 1);
	
	[saw, pos]
}.plot;
)

But this will be exact:

(
{
	var width = 0.25;
	var freq = 400;
	var saw = LFSaw.ar(freq/2, 1).linlin(-1, 1, width.neg, 1-width);
	var pos = Phasor.ar(Impulse.ar(freq/2), freq/2 * SampleRate.ir.reciprocal, width.neg, 1-width, width.neg);
	//var pos = Phasor.ar(0, freq * BufFrames.ir(b) * SampleRate.ir.reciprocal, 0, BufFrames.ir(b));
	
	saw = saw.bilin(0, width.neg, 1-width, 0, -0.5, 1);
	pos = pos.bilin(0, width.neg, 1-width, 0, -0.5, 1);
	
	[saw, pos]
}.plot;
)

thanks so much :slight_smile:

can Shaper generally be exchanged with BufRd? im not getting the same results.

b = Buffer.alloc(s, 512, 1, { |buf| buf.chebyMsg([1,0,1,1,0,1])});
(
{
	var width = MouseX.kr().clip(0.1,0.9);
	var freq = MouseY.kr(200, 1000);
	var pos = Phasor.ar(Impulse.ar(freq/2), freq/2 * SampleRate.ir.reciprocal, width.neg, 1-width, width.neg);
	var sine, shaper, playbuf;
	
	pos = pos.bilin(0, width.neg, 1-width, 0, -0.5, 1);
	sine = (Select.ar(pos>0, [0.5+pos, pos])*2pi).sin;
	
	//shaper = Shaper.ar(b, sine, 0.5);
	playbuf = BufRd.ar(1, b, sine, interpolation:4);
	
	//shaper.dup
	playbuf.dup
}.scope;
)

do you know how to get the variable-slope triangle wave out of the folded one as described in chapter 2.4 “tilted triangular fractional period phase signal”? many thanks :slight_smile:


(
{
	var width = 0.25;
	var freq = 400;

	var tri = LFTri.ar(freq/2, 0, LFTri.ar(0.2).range(1,2)).fold(-1,1);
	var saw = LFSaw.ar(freq/2, 1).linlin(-1,1, width.neg, 1-width);
	
	saw = saw.bilin(0, width.neg, 1-width, 0, -0.5, 1);
	saw = Select.ar(saw>0, [0.5+saw, saw]);

	[saw, tri]
}.plot;
)

I have still some further questions, maybe you can help:

1.) is sine ment to be the final output? in the paper its declared as a waveshaper, so it should always be applied to another signal, right?
2.) when applying sine (waveshaper) to another signal should it be multiplied or should it modulate the phase (this is kind of redundant or?) saw is ment to be the phaseshaper right?
3.) is it necessary to divide the freq always by 2 in the LFSaw ? when sine is the final output the freq/2 causes a freq down shift of the final sound

thank you very much :slight_smile:

1 Like

Is this what you mean:

({
	var width = MouseX.kr;
	var phasor = LFSaw.ar(MouseY.kr(100, 1000)).range(width.neg, 1-width);
	phasor = phasor.bilin(0, width.neg, 1-width, 0, -1, 1).abs*2-1;
	phasor.dup*0.1
}.scope)

Have you looked at the Maths quark? The Maths is basically a variable slope triangle (I think), with a slope you can warp towards exponential or logarithmic. The quark is implemented in Faust, but this thread made me realize I could do it is SC as well. Below is the working code in SC. I will fold that into the quark when I have the time. To use this, just save this into a .sc file and save it into the extensions folder. The help for Maths will work with Maths2. They should give the exact same results.

Maths2 {
	*ar {|rise=0.1, fall=0.1, linExp = 0.5, loop = 1, plugged = 0, trig = 0|

		var freq = 1/(rise.clip(0.001,10*60)+fall.clip(0.001,10*60)), width=rise.clip(0.001,10*60)/(rise.clip(0.001,10*60)+fall.clip(0.001,10*60));

		var plugTrig = Trig1.ar(1-plugged, 1/SampleRate.ir);
		var loopTrig = Trig1.ar(loop, 1/SampleRate.ir);

		var phasor = Phasor.ar(Silent.ar+loopTrig+plugTrig, 2*freq/SampleRate.ir, -1, 1, -1);

		var phasorTrig = Trig1.ar(0.5-phasor, 1/SampleRate.ir)+EnvGen.ar(Env([0,0,1,0], [1/SampleRate.ir,1/SampleRate.ir,1/SampleRate.ir]));
		var latchTrig = (phasorTrig+(DelayN.ar(loopTrig, 0.01, 0.01))).clip(0,1);
		var postEnv =(Latch.ar(K2A.ar(loop), latchTrig)>0);


		var eof, eor;


		var inTrig = Trig1.ar(trig, 1/SampleRate.ir);
		var phasor2 = Phasor.ar(inTrig, 2*freq/SampleRate.ir, -1, 1, -1);
		var postEnv2 = SetResetFF.ar(Delay1.ar(inTrig), Trig1.ar(0.5-phasor2, 1/SampleRate.ir));
		var maths, maths2, interp;

		phasor = phasor.linlin(-1,1,width.neg, 1-width);
		maths = phasor.bilin(0, width.neg, 1-width, 0, -1, 1);
		maths = 1-(maths.abs);
		maths = maths*postEnv;

		phasor2 = phasor2.linlin(-1,1,width.neg, 1-width);
		maths2 = phasor2.bilin(0, width.neg, 1-width, 0, -1, 1);
		maths2 = 1-(maths2.abs);
		maths2 = maths2*postEnv2;

		maths = Select.ar(plugged, [maths,maths2]);

		interp = Select.kr(linExp>0.5, [linExp.linlin(0, 0.5, 1, 0) , linExp.linlin(0.5, 1, 0, 1)]);
		maths = Select.ar(linExp>0.5, [ maths-1, maths]);
		maths = (maths**8*interp)+(maths*(1-interp));
		maths = Select.ar(linExp>0.5, [ maths+1, maths]);

		eof = Select.ar(plugged, [(phasor*postEnv).neg>0, (phasor2*postEnv2).neg>0]);
		eor = Select.ar(plugged, [(phasor*postEnv)>0, (phasor2*postEnv2)>0]);

		^[maths, eof, eor]
	}
}

hey thanks :slight_smile:

(
{
	var width = 0.25;
	var freq = 400;
	var phasor = LFSaw.ar(freq/2).range(width.neg, 1-width);
	phasor = phasor.bilin(0, width.neg, 1-width, 0, -1, 1).abs*2-1;
	[phasor]
}.plot
)

this gives control over the tilt, yes!
in the paper its also folded, do you have an idea how to adjust the code?
in the paper it looks like this:
Slope

2.) many thanks. i will have a look :slight_smile:

i think this is doing it now, but im not sure about the right amounts for Fold.ar and here has something to be changed as well sine = (Select.ar(phasor>0, [0.5+phasor, phasor])*2pi).sin; for the tilted triangle i think, or?

(
u = Signal.sineFill(512, [1]);
b = Buffer.loadCollection(s, u, 1);
)

//variable-slope ramp

(
{
	var width = 0.25;
	var freq = 400;
	var sine, shaper;
	var bufFrames = BufFrames.ir(b);
	var phasor = Phasor.ar(Impulse.ar(freq/2), freq/2 * SampleRate.ir.reciprocal, width.neg, 1-width, width.neg);
	phasor = phasor.bilin(0, width.neg, 1-width, 0, -0.5, 1);
	sine = (Select.ar(phasor>0, [0.5+phasor, phasor])*2pi).sin;
	shaper = BufRd.ar(1, b, sine.range(0, bufFrames-1));
	
	[phasor, sine, shaper]
}.plot
)


//tilted triangle 
(
{
	var width = 0.25;
	var freq = 400;
	var sine, shaper;
	var bufFrames = BufFrames.ir(b);
	var phasor = Phasor.ar(Impulse.ar(freq/2), freq/2 * SampleRate.ir.reciprocal, width.neg, 1-width, width.neg);
	phasor = phasor.bilin(0, width.neg, 1-width, 0, -1, 1).abs*2-1;
	phasor = Fold.ar(phasor, -0.5, 0.5);
	sine = (Select.ar(phasor>0, [0.5+phasor, phasor])*2pi).sin;
	shaper = BufRd.ar(1, b, sine.range(0, bufFrames-1));
	
	[phasor, sine, shaper]
}.plot
)

It is very close. Try it with Wrap instead of Fold:

({
	var width = MouseX.kr;
	var phasor = LFSaw.ar(MouseY.kr(100, 1000)).range(width.neg, 1-width);
	phasor = (phasor.bilin(0, width.neg, 1-width, 0, -1, 1).abs*1.5).wrap(0,1);
	phasor.dup
}.plot)

wrap, fold, and clip are your friends. Look those up in the help.

Sam

thanks. unfortunately the extension for maths2 is not working.
i get the error message:

ERROR: Select arg: ‘which’ has bad input: false

  • Select requires a UGen input for the index.
  • An index such as linExp > 0.5 is a UGen if linExp is a UGen.
  • In a SynthDef, arguments are automatically promoted to UGens (outputs of a Control). So one might have expected argument linExp → promoted to UGen → comparison op → UGen result → Select would be fine.
  • But an *ar method of a pseudo-UGen class does not promote arguments to UGens. If you pass a fixed number for linExp, it will do e.g. 0 > 0.5 and produce false and break Select.

But Maths2 could detect the Boolean comparison results and prune branches that will never be used (which would be more efficient than simply wrapping numbers in UGens).

hjh