Super Cool SuperCollider Synthesis Challenge (Prize)?

Lol, if Massive (and later Serum) are the synths that more or less created dubstep - Razor is the synth that created… whatever clubby computer music it is that Gabor Lazar, Rian Treanor, Cam Deas, and Errorsmith etc etc make. (Errorsmith made the synth, so I guess it’s no surprise that he uses it…)

2 Likes

hey, @nathan helped me out with creating the spectrum and the initial idea of the quasi bandpass filter sweep trough the partials and ive added some envelopes.

(
SynthDef(\gabor, {

	var trig = \trig.tr;
	
	var freqEnv = EnvGen.kr(Env([1,2,1], [0.5, 0.01], \lin), trig, doneAction: Done.none);
	var fltEnv = EnvGen.kr(Env([0.0125,1,0.0125], [\fAtk.kr(0.3), \fRel.kr(0.01)], \exp), trig, doneAction: Done.none);
	var gainEnv = EnvGen.kr(Env([0,1,0], [\atk.kr(0.01), \rel.kr(0.3)], \lin), trig, doneAction: Done.none);

	var numPartials = 50;
	var n = (1..numPartials);
	var freqs = \freq.kr(150) * (n + (n * n * 0.05));
	var cutoff, amps, sig;

	cutoff = fltEnv * \fltRange.kr(8000);

	amps = (
		(freqs.log2 - cutoff.log2).squared
		* \slope.kr(-5)
	).dbamp;

	sig = SinOsc.ar(freqs * freqEnv.midiratio, 0, amps).sum * -25.dbamp;

	sig = sig * gainEnv;

	sig = Pan2.ar(sig, \pan.kr(0), \amp.kr(0.25));
	Out.ar(\out.kr(0), sig);
}).add;
)

(
Pdef(\gabor,
	Pbind(
		\instrument, \gabor,
		
		\atk, 0.01,
		\rel, 0.5,
		
		// Bandpass Filter sweep
		\slope, -5,
		\fltRange, 8000,
		
		\fAtk, 0.3,
		\fRel, 0.01,
		
		\freq, 68,
		
		\amp, 0.30,
		\pan, 0,
		\out, 0,
	)
).play;
)

Pdef(\gabor).stop;


ive also played around with this BufWr approach to control the amplitudes of the partials and modulating the phase of the LFGauss for a similiar bandpassfilter like sweep. I also tried to use control busses to map different LFOs / looping envelopes to the phase argument of LFGauss for multichannel expansion and split the signal into different audio busses, to have two seperate modulated signals. but without the additional phase modulation you dont really have any possibility to shape the spectrum, when the synth is running. any further ideas?

(
~numPartials = 50;
~bufAmps = Buffer.alloc(s, ~numPartials, 1);

SynthDef(\additive, {
	arg index=1, iScale=3, gate=1, time=1;
	
	var numPartials = ~numPartials;
	var bufAmps = ~bufAmps;
	var f0 = \freq.kr(68);
	var sig, mod;
	
	var iEnv = EnvGen.kr(Env([index, index * iScale, index], [\iAtk.kr(0.2), \iRel.kr(0.5)], \lin), gate, timeScale: time, doneAction: Done.none);
	var gainEnv = EnvGen.kr(Env.linen(\atk.kr(0.1), \sus.kr(0.5), \rel.kr(1), curve: \sine), gate, doneAction: Done.freeSelf);
	
	BufWr.ar(
		LFGauss.ar(
			duration: SampleDur.ir * numPartials * \factor.kr(1, 0.5).reciprocal,
			width: \width.kr(0.2, 0.5),
			iphase: LFTri.ar(\phaseModFreq.kr(0.5)).linexp(-1, 1, 1, 2),
		),
		bufnum: bufAmps,
		phase: Phasor.ar(end: numPartials)
	);
	
	mod = SinOsc.ar(f0 * \mRatio.kr(1), mul: iEnv);
	
	sig = Array.fill(numPartials, {|i|
		var freqs, partials;
		freqs = f0 * (i + (i * i * 0.05));
		partials = SinOsc.ar(
			freq: freqs * \cRatio.kr(1),
			phase: mod.wrap(0, 4pi),
			mul: Index.ar(bufAmps, i)
		) / numPartials;
	}).sum;
	
	sig = LeakDC.ar(sig);
	
	sig = sig * gainEnv;
	
	sig = Pan2.ar(sig, \pan.kr(0), \amp.kr(0.25));
	Out.ar(\out.kr(0), sig);
}).add;
)

(
Pdef(\additive,
	Pbind(
		\instrument, \additive,
		
		\dur, 4,
		
		\width, 0.20,
		\phaseModFreq, 0.5,
		
		\atk, 1.5,
		\sus, 4,
		\rel, 2.5,
		
		\time, Pfunc { |ev| ev.use { ~sustain.value } / thisThread.clock.tempo },
		
		//Phase Modulation
		\iAtk, 1.5,
		\iRel, 2.5,
		\mRatio, 1.96417,
		\cRatio, 2.28158,
		\index, 1,
		\iScale, 1,
	
		\amp, 0.30,
		\out, 0,
	)
).play;
)

4 Likes

This is great.

I’ve been considering building an efficient “Sine Bank” ugen specifically for this purpose (e.g. to read freq/amp/phase data from a buffer and render a bank all at once). I think the additional piece to make things usable is to mass-manipulate values in those buffers, which requires a handful of new UGens for this purpose. For example, I would expect that I could do something like this (…imaging a Bank class encapsulating these buffer ops):

var numPartials = 256;
var fundamental = \freq.kr(440);
var warpPhase, freqBank, ampBank;

warpPhase = SinOsc.kr(4).range(-4, 4);

freqBank = Bank.fillSeries(0, 1, numPartials); // [0,1,2...]
freqBank = fundamental * buf;
freqBank = freqBank + Bank.fillSine(freq: 0.1, phase:warpPhase, amp: 4); // offset each freq

ampBank = 1 - Bank.fillSeries(0, 1/numPartials, numPartials).pow(0.25); // amp falloff... 

sig = SineBank.ar(freqBank, amp: ampBank);

Implementing this isn’t so challenging … I think the challenge is figuring out the minimal set of Bank operations you need in order to do anything interesting.

(I guess it’s worth noting that you can already do this with existing SC ugens, it’s just too slow to scale to 256+ partials for things as complex as Razor is doing)

3 Likes

this would be really great :slight_smile: lately there have been a lot of requests on the forum about synthesis related to razor (myself included). its really interesting that one of the first things i picked up about sc is the strength to do iteration for additive synthesis pretty easily. but to control all the partials with exisiting ugens isnt so trivial / possible.

do you have a good way to deal with additive synthesis in a universal way until we have the sine bank ;)?
imagine for example i want to have only the fundamental Ab2 and the 7th partial F#5 (-31 dev. from 12TET) of the harmonic series with specific amplitudes for example sig = SinOsc.ar([103.826, 726.783 * 2], 0, [0.5, 0.6]).sum;
and another time more partials with a specific amplitude conture for example all even partials up to a specific overtone. i would like to control everything with patterns and not write a new SynthDef every single time for only one specific case. any ideas ? :slight_smile:

(
(1..50).do({|partials|
	SynthDef(\additive ++ partials, {

		var sig, freqs, gainEnv;

		gainEnv = EnvGen.ar(Env.adsr(
			\atk.kr(0.07),
			\dec.kr(0.5),
			\sus.kr(1),
			\rel.kr(2),
			curve: \curve.kr(-4)
		), \gate.kr(1), doneAction:2);

		freqs = Array.fill(partials, {|i|
			\freq.kr(20) * (i+1); // even partials
		});

		sig = freqs.collect({|freq, i|
			var amps = \decay.kr(0.5) / (i+1); // even partials
			SinOsc.ar(freq) * amps;
		});

		sig = Mix(sig);

		sig = sig * gainEnv * \amp.kr(0.3) * \vel.kr(1);
		sig = Splay.ar(sig);
		Out.ar(\out.kr(0), sig)
	}).add;
});
)

x = Synth(\additive10, [\freq, 110, \decay, 0.5]);

x.set(\gate, 0);

Have you looked at DynKlang?

Sam

hey thanks for your help. ive never really investigated deeply. because i thought the values are set and cant be changed, after the SynthDef has been evaluated. i will have a look again if it suits my needs.
is DynKlang the best way to deal with additive synthesis in SC?

EDIT: i confused it with the Klang Ugen.

I don’t think DynKlang would solve any of the efficiency problems. As the help file says, “it is basically a wrapper around SinOsc UGens in order to provide a similar interface to Klang.”

Using an inverse fourier transform should be an efficient approach to additive synthesis (at the expensive of some time and frequency resolution).

i think for the case of having two sinewaves with specific frequencies and amplitudes its not the right approach.

I tried to implement an additive Synth using IFFT. It almost works, but i get some clicks and pops and i dont know why :frowning:
It reads the amplitudes of the partials from a 256-channel control bus and packs them into a FFT chain. The chain is converted to a waveform using IFFT and an OSC plays the waveform.

In the example the 256-channel-control bus for the amplitudes is controlled by a Ndef which you can control with the mouse

Code
~numPartials = 256;
// Add the SynthDef
(
SynthDef(
	\ifft,
	{
		arg freq=440, gate=1, amp=0.6, ampB=0.6, out=#[0,1],
		atk=0.01, rls=0.2, catk=4, crls=4.neg, sustain=0,
		ampBus;
		var env, sig, phases, chainBufSize, mags, buffer, chain,
		phasor, numPartials;

		chainBufSize = 512;
		numPartials = ~numPartials ? 256;

		mags = In.kr(
			ampBus,
			numPartials
		);
		phases = {pi.rand}!(numPartials-1);
		mags = [0] ++ mags;
		phases = [0,0] ++ phases;
		mags = mags * chainBufSize / 4;

		buffer = LocalBuf( chainBufSize );

		chain = FFT(
			buffer,
			DC.ar( 0 )
		);
		chain = PackFFT(
			chain,
			chainBufSize,
			[ mags, phases ].flop.flatten,
			0,
			numPartials,
			1
		);
		IFFT(
			chain,
			1
		);
		phasor = Phasor.ar(
			1,
			chainBufSize * freq / SampleRate.ir,
			0,
			chainBufSize
		);
		sig = BufRd.ar(
			1,
			buffer,
			phasor
		);

		env = Env.new(
			[0, amp, amp, 0],
			[atk, (sustain - atk) max: 0, rls],
			[catk, 0, crls]
		).kr(2);
		sig = sig * env;
		Out.ar( out, sig );
	}
).add;
)

// Create the Ndef that controls the amplitudes of the partials
(
Ndef(\control).clear;
Ndef(\control,
	{
		var width, center, x, left, right, values, numPartials;
		center = MouseX.kr(0.0,1.0);
		width = MouseY.kr(0.1,0.6);
		numPartials = ~numPartials ? 256;

		numPartials = numPartials - 1;
		x = ( 0 .. numPartials ) / numPartials;
		left = (2/width) * ( x - center + ( width/2 ) );
		left = left.lincurve(0, 1, 0, 1, -10);
		right = (2/width) * ( x - center );
		right = right.lincurve(0, 1, 0, 1, -10);
		values = left - right;

		values.collect( { |x| DC.kr(0) + x } );
	}
);
)

(
Pdef(\ifft,
	Pbind(
		\instrument, \ifft,
		\scale, Scale.major,
		\ampBus, Ndef(\control).bus.index,
		\scale, Scale.major,
		\degree, Pseq([ 0,2,4,3 ],inf) + Pseq([[0,2,4],[1,2,5],[0,4,6]],inf) ,
		\db, -25,
		\harmonic, 0.5,
		\dur, 1,
		\atk, 0.5,
		\catk, -7,
		\rls, 1.2,
		\legato, 0.6,
	)
)
)

Pdef(\ifft).play
2 Likes

Another strategy is to precalculate the IFFT into a table, and then use that table to play back.
I once made a proof of concept: supercollider implementation of padsynth algorithm. (the precalculation was rather expensive if I recall correctly, but it calculated many more tables than strictly needed, so it could be heavily optimised still).

Edit: upon retrying that code, it seems in some circumstances it also generates pops and clicks. I’d have to investigate exactly what goes wrong…