Replicating "Aliasing Synth", a gem in Reaktor's User Library

I’m profoundly in love of this esoteric machine, which i have used widely in the past:

As one can read in the reaktor’s UL which hosts the synth here: https://www.native-instruments.com/es/reaktor-community/reaktor-user-library/entry/show/5241/:

Audiotable is read and written at the same time but in different speeds. Because of the “careless” speeds it produces many errors. That’s good.

finest moves in speed cause surprising and dramatic changes in sound.
And 5 voices can be very fat.

Hipass+FM, Lopass+FM and a Notch behind it form the bizarr error sound to more or less ear-candy atmos.

I would really love to have something similar working in supercollider, but i wouldn’t know even how to start. Does anyone have an idea on how to approach the development of a synthdef that mimics this aliasing behavior?

Can you open up the patch to see how they made it in reaktor? that might give some more specific inspiration.

but here’s a 5 voice buffer that is written to and read from at different speeds:

b = Buffer.alloc(s, s.sampleRate, 5);

(
{
  var writeSig, writePos, readPos;
  
  writeSig = SinOsc.ar({ LFDNoise3.kr(1).exprange(100, 1000) } ! 5);
  writePos = Phasor.ar(0.0, 1.0 /*90*/, 0.0, BufFrames.kr(b));
  BufWr.ar(writeSig, b, writePos);
  
  readPos = Phasor.ar(0.0, 100, 0.0, BufFrames.kr(b));
  Splay.ar(BufRd.ar(5, b, readPos));
}.play;
)
1 Like

Wow @Eric_Sluyter! that’s an awesome start!
I’m always surprised by how compact the supercollider code can be… really amazing…

Simply tweaking the read and write phasor rates perfectly captures the essence of the synth.

b = Buffer.alloc(s, s.sampleRate, 5);

(
{
var writeSig, writePos, readPos;
writeSig = SinOsc.ar({ LFDNoise3.kr(1).exprange(10, 100) } ! 5);
writePos = Phasor.ar(0.0, MouseX.kr(0,200), 0.0, BufFrames.kr(b));
BufWr.ar(writeSig, b, writePos);
readPos = Phasor.ar(0.0, MouseY.kr(0,100), 0.0, BufFrames.kr(b));
Splay.ar(BufRd.ar(5, b, readPos));
}.play;
)

Actually, most of the “fat” character of the reaktor ensemble comes from the saturator filter, which seems easy to replicate (i’m going for that now!).

Some differences i can see from Eric’s implementation and what i can see in the synth:

-Interestingly, in the original synth the readpos signal is used also as the writesignal (wtf?)

-Both readpos and writepos phasor rates (pitch) can be modulated with a “multichannel expanded” LFO, which creates a “thicker” and more dynamic sound when distributed in the stereo field. This is similarly achieved in Eric’s implementation by having a multichannel expanded writesig.

-The phasors in the reaktor ensemble are asymmetric triangle oscillators, which can skew from saw to triangle to inverted saw. In my understanding, this creates weird behaviors on the writepos signal, and allows to read the buffer in any direction

-The saturator filter is really cool and gives the sound an important part of it’s character. It’s 2 serial HP-LP 4pole filters with some massive saturation.

-There’s a notch EQing stage before the saturator filter, which helps domesticating the beast.

I made some screenshots of the panel and structure for inspiration.
I guess @gentleclockdivider might be interested on this thread…






I’ll try to come back soon with a replica of the filter… so excited to have this started! thanks Eric, the results are cool and promising!

A first implementation of the series HP4-LP4 FM filter saturator.
Beware, this is loud!

//a serial HP-LP FM filter with saturation
(
SynthDef.new(\satfmfilter, {
	arg in=0,out=0;
	var input,filteredHP,filteredLP,inputLPfm,inputHPfm,finalsig;
	var lppitch=\lppitch.kr(90).midicps; //0-130
	var lpres=\lpres.kr(0.7);
	var hppitch=\hppitch.kr(20).midicps;
	var hpres=\hpres.kr(0.7);
	var lpfm=\lpfm.kr(0); //0-3000
	var hpfm=\hpfm.kr(0);
	var drive=\dbdrive.kr(10); //0-30db

	input = In.ar(in, 1);
	input=SinOsc.ar(60);

	//////////////////////for testing
	lpfm=MouseX.kr(0,3000);
	//hpfm=lpfm;
	
	lppitch=MouseY.kr(0,130);
	hppitch=lppitch;
	//////////////////////
	
	inputLPfm=input*lpfm; //we use the input also as an FM source
	inputHPfm=input*hpfm;

	filteredHP=BHiPass4.ar(input,hppitch+inputHPfm,hpres,drive.dbamp).tanh;//LP filter and saturation
	filteredLP=BLowPass4.ar(filteredHP,lppitch+inputLPfm,lpres); //HP filtering in series

	finalsig=LeakDC.ar(filteredLP,0.995,drive.dbamp).tanh; //final saturation and dc correction

	Out.ar(out, finalsig*0.3);

}).play();
)

Next step is trying this with Eric’s algorithm, and see how close we get!

1 Like

yesssss

b = Buffer.alloc(s, s.sampleRate, 5);

(
SynthDef.new(\aliasingSynth, {
	//Aliasing Synth.
	//Based on a Reaktor Ensemble by Dietrich Pank
	//https://www.native-instruments.com/es/reaktor-community/reaktor-user-library/entry/show/5241/:
	
	arg in=0,out=0;
	var writeSig, writePos, readPos;
	var aliased,filteredHP,filteredLP,inputLPfm,inputHPfm,finalsig,writef,readf;
	
	var readpitch=\readpitch.kr(24!5).midicps;
	var writepitch=\writepitch.kr(35!5).midicps;
	var readlfofreq=\readlfofreq.kr(0.3!5)*12; //normalized input
	var writelfofreq=\writelfofreq.kr(0.13!5)*12;	
	var readlfoamp=\readlfoamp.kr(1!5); //normalized input
	var writelfoamp=\writelfoamp.kr(0!5);
	
	var lppitch=\lppitch.kr(120).midicps; //0-130
	var lpres=\lpres.kr(0.7);
	var hppitch=\hppitch.kr(20).midicps;
	var hpres=\hpres.kr(0.7);
	var lpfm=\lpfm.kr(1000); //0-3000
	var hpfm=\hpfm.kr(0);
	var drive=\dbdrive.kr(10); //0-30db
	
	//aliasing synth (5 voices)
	writef=SinOsc.kr(writelfofreq,0,writelfoamp)+writepitch;
	readf=SinOsc.kr(readlfofreq,0,readlfoamp)+readpitch;

	///////////////just for testing
	writef=MouseX.kr(0,36).midicps;
	readf=MouseY.kr(0,36).midicps;
	//////////////
	
	writeSig = SinOsc.ar({ LFDNoise3.kr(1).exprange(10, 100) } ! 5); //signal to be written in the audiotable
	writePos = Phasor.ar(0.0, writef, 0.0, BufFrames.kr(b)); //write position header
	readPos = Phasor.ar(0.0, readf, 0.0, BufFrames.kr(b)); //read position header
	BufWr.ar(writeSig, b, writePos); //in the original synth, writeSig=readPos
	
	
	aliased = Splay.ar(BufRd.ar(5, b, readPos)); //aliasing synthesis result

	
	//filter  (stereo)
	inputLPfm=aliased*lpfm; //we use the input also as an FM source
	inputHPfm=aliased*hpfm;

	filteredHP=BHiPass4.ar(aliased,hppitch+inputHPfm,hpres,drive.dbamp).tanh;//LP filter and saturation
	filteredLP=BLowPass4.ar(filteredHP,lppitch+aliased,lpres); //HP filtering in series

	finalsig=LeakDC.ar(filteredLP,0.995,drive.dbamp).tanh; //final saturation and dc correction

	Out.ar(out, finalsig*0.1);

}).play();
)
1 Like

What is the buffer in ‘b’ and should it be a localBuf in the synthdef or an argument to the synth?

Hey Thor! i just edited my previous code. b is just an empty allocation of the buffer, as it is being written and read inside the synthdef. Try now (and beware your speakers) moving and stopping the mouse in different positions of the screen. It is sonic mayhem, so don’t expect any beauty!

Maybe it would be easier to use a LocalBuf inside the synthdef in that case. Sounds pretty crazy like intended:)

1 Like

aaaa thanks for the tip! i didnt know buffers could be allocated inside the synthdef… way more convenient!!!

Here it is, using localBuf, and restricting it to a size of 1024 like the original reaktor ensemble.
Similarly, i got rid of the separate write signal, and used the readpos both to point the read position and content of the buffer. This would be some sort of feedback, right?

Anyway, the beast sounds much more concise and wicked now. There appear very interesting periodic artiffacts. I suspect this can be refined to a much more controlled state; sadly my knowledge with SC is still very limited, but learning every day with this amazing community!


(
SynthDef.new(\aliasingSynth, {
	//Aliasing Synth. Based on a Reaktor Ensemble by Dietrich Pank.
	//https://www.native-instruments.com/es/reaktor-community/reaktor-user-library/entry/show/5241/:
	//Coded by Santi Vilanova (Playmodes) and Eric Sluyter, with the invaluable help of Thor_Madsen


	arg in=0,out=0;
	var b, writeSig, writePos, readPos;
	var aliased,filteredHP,filteredLP,inputLPfm,inputHPfm,finalsig,writef,readf;

	var readratio=\readratio.kr(1!5); //0-5
	var readfine=\readfine.kr(0!5); //-0.5 0.5
	var writeratio=\writeratio.kr(1!5); //0-5
	var writefine=\writefine.kr(0!5); //-0.5 0.5
	var transpose=\transpose.kr(0!5); //-2 2

	var readlfofreq=\readlfofreq.kr(0.3!5)*12; //normalized input
	var writelfofreq=\writelfofreq.kr(0.13!5)*12;
	var readlfoamp=\readlfoamp.kr(0!5); //normalized input
	var writelfoamp=\writelfoamp.kr(0!5);

	var lppitch=\lppitch.kr(120).midicps; //0-130
	var lpres=\lpres.kr(0.7);
	var hppitch=\hppitch.kr(20).midicps;
	var hpres=\hpres.kr(0.7);
	var lpfm=\lpfm.kr(1000); //0-3000
	var hpfm=\hpfm.kr(0);
	var drive=\dbdrive.kr(10); //0-30db

	b = LocalBuf.new(1024,5);

	//aliasing synth (5 voices)
	//writef=SinOsc.kr(writelfofreq,0,writelfoamp)+writeratio+writefine+transpose;
	//readf=SinOsc.kr(readlfofreq,0,readlfoamp)+readratio+readfine+transpose;

	///////////////just for testing
	writef=MouseX.kr(0,5)!5;
	readf=MouseY.kr(0,5)!5;
	//////////////

	writePos = Phasor.ar(0.0, writef, 0.0, BufFrames.kr(b)); //write position header
	readPos = Phasor.ar(0.0, readf, 0.0, BufFrames.kr(b)); //read position header
	BufWr.ar(readPos, b, writePos); //in the original synth, writeSig=readPos


	aliased = Splay.ar(BufRd.ar(5, b, readPos)); //aliasing synthesis result


	//filter  (stereo)
	inputLPfm=aliased*lpfm; //we use the input also as an FM source
	inputHPfm=aliased*hpfm;

	filteredHP=BHiPass4.ar(aliased,hppitch+inputHPfm,hpres,drive.dbamp).tanh;//LP filter and saturation
	filteredLP=BLowPass4.ar(filteredHP,lppitch+aliased,lpres); //HP filtering in series

	finalsig=LeakDC.ar(filteredLP,0.995,drive.dbamp).tanh; //final saturation and dc correction
	finalsig=Limiter.ar(finalsig*0.1);
	Out.ar(out, finalsig);

}).play();
)

Already working within my software framework!

last aliasing synth codes here, together with my personal collection:
https://github.com/PlaymodesStudio/oceanodeSynthdefs

3 Likes

this looks cool! how are you interfacing with SC in your software?

(also the link gives me a 404 error?)

As this is programmed in openframeworks, I’m interfacing thrugh ofxsupercollider.
https://www.erase.net/projects/ofxSuperCollider/

I just realized my github repository was still private, hence the 404!
I made it public, it should work now.

Eric, in the original synth the read/write operations are done through an assymmetrical triangle (tri/par symm) instead of a ramp. how could we do that?

https://scsynth.org/uploads/default/original/2X/7/787ee934e65c9a9429572e23236df089a556502d.png

the link now works for me, thanks :slight_smile:

for an asymmetrical triangle, use VarSaw:

{ VarSaw.ar(400, width: Line.kr(0, 1, 0.01)) }.plot() 

e.g.:

(
{ 
  var writef = 5, frames = 1024; 
  [
    Phasor.ar(0, writef, 0, frames), 
    VarSaw.ar((SampleRate.ir / frames) * writef, 0, 0.2).range(0, frames)
  ] 
}.plot
)

but if you are using the readPos as the writeSig, you might want to keep it bipolar (-1 to 1) for the writing stage and only map it using e.g. .range when you use it as the actual read position.

1 Like

One way to do this is to use a looping Env:

// scope
(
{
	var outLevel = 0.2;
	var freq = 100;
	var proportion = LFTri.kr(0.25).range(0.5, 1);
	Env([0, 0, 1, 0, 0], [0, proportion, 1 - proportion, 0], releaseNode: 3, loopNode: 0)
	.ar(gate: 1, timeScale: freq.reciprocal, levelScale: outLevel);
}.scope;
)
// plot
(
{
	var freq = 10;
	var proportions =  [0.5, 0.7, 0.9, 1];
	proportions.collect({arg prop, i;
		Env([0, 0, 1, 0, 0], [0, prop, 1 - prop, 0], releaseNode: 3, loopNode: 0)
		.kr(gate: 1, timeScale: freq.reciprocal);
	})
}.plot(0.5);
)

Best,
Paul

3 Likes

That’s really a nice piece of code, and I highly enjoy the sound of the modulating tri-shape.

1 Like

thanks TXMod!
i’ll try this update soon, see how it behaves with the synth.
I agree with Thor, the tri-saw modulation is very cool! maybe the foundation for a new synth? makes me think of the Vital morphing oscillators…

1 Like

For synthesis, modulating the curve is also very effective:

// scope
(
{
	var outLevel = 0.2;
	var freq = 100;
	var curve = LFTri.kr(0.125).range(-10, 10);
	Env([0, 0, -1, 0, 1, 0, 0], [0, 0.25, 0.25, 0.25, 0.25, 0], 
		curve: [curve, curve.neg, curve, curve.neg, curve], 
		releaseNode: 5, loopNode: 0)
	.ar(gate: 1, timeScale: freq.reciprocal, levelScale: outLevel);
}.scope;
)
// plot
(
{
	var freq = [100, 100.5];
	var curves =  [-10, -5, 0, 5, 10];
	curves.collect({arg curve, i;
		Env([0, 0, -1, 0, 1, 0, 0], [0, 0.25, 0.25, 0.25, 0.25, 0], 
			curve: [curve, curve.neg, curve, curve.neg, curve], 
			releaseNode: 5, loopNode: 0)
		.kr(gate: 1, timeScale: freq.reciprocal);
	})
}.plot(0.5);
)

Best,
Paul

1 Like

Another really cool one! The aliasing seems pretty server but I don’t really understand what is causing it.

Yes that’s one issue with any sharp-cornered waveforms - they cause noticeable aliaising at high frequencies. The same is true with LFSaw and LFTri played high up.
Compare them to Saw:

// aliasing
{LFSaw.ar(LFTri.kr(0.5).range(1000, 8000), mul: 0.1)}.play;
{LFTri.ar(LFTri.kr(0.5).range(1000, 8000), mul: 0.1)}.play;
// no aliasing
{Saw.ar(LFTri.kr(0.5).range(1000, 8000), mul: 0.1);}.play;

2 Likes