Implementing filters with additive synthesis

Inspired by Errorsmith’s Razor I am trying to implement ‘filters’ using additive synthesis but I am struggling to find the right approach. My goal here is to implement a saw wave with a sweeping low pass ‘filter’, however, the ‘filtering’ is achieved by setting the amplitudes of certain harmonic frequencies in the Fourier series to 0. Eventually, I am also interested in more complex filters, like ones with variable slope or multiple band rejects.

Here is my first try: First I calculate the frequencies and corresponding amplitudes of a saw wave, then I manipulate the amplitudes with a simple if-then-function, the cutoff frequency here is 2000 Hz:

(
{
var n = 30;
var f0 = 200;
var freqs = Array.fill(n, {arg i; f0*(i+1)});
var amps = Array.fill(n, {arg i; 1/(i+1)});
var filtered_amps = n.collect {arg i; if (freqs[i] < 2000, {amps[i]}, {0})};
var sig = Mix.ar(SinOsc.ar(freq:freqs, mul:filtered_amps));
Pan2.ar(sig*0.5, 0.0);
}.play;
)

This all works great, but if I want to change the cutoff frequency with MouseX, it doe not work, which leads me to believe this sort of array manipulation in real time might not be the right approach after all. Is there a better approach to this type of synthesis technique, one that also scales well with more complex functions?

the problem you are having is that the assignment of filtered_amps is happening once when the function is interpreted - to my knowledge there’s no convenient ‘if’ logic inside a running Synth. My first thought would be to use a multichannel control and have the logic outside the synth…

have a look at the help file for DynKlank …

1 Like

if is a server side operation, you want SelectX.kr

(
{
var n = 30;
var f0 = 200;
var freqs = Array.fill(n, {arg i; f0*(i+1)});
var amps = Array.fill(n, {arg i; 1/(i+1)});
var filtered_amps = n.collect {arg i; SelectX.kr(freqs[i] < MouseX.kr(80,10000,\exponential), [ amps[i] , 0 ])};
var sig = Mix.ar(SinOsc.ar(freq:freqs, mul:filtered_amps));
Pan2.ar(sig*0.5, 0.0);
}.play;
)
2 Likes

Hi and welcome,

@semiquaver and @hemiketal have pointed to the issue in your first approach. There’s an ‘if’ in the language and ‘if’ in the server. See this help file:

https://supercollider.github.io/tutorials/If-statements-in-a-SynthDef.html

To apply boolean logic server-side you have to think about signals returning 0 an 1. However, you’ve been very close. Your example works if the Functions within ‘if’ are dropped, making it to an ‘if’-UGen.

(
{
	var n = 30;
	var f0 = 200;
	var freqs = Array.fill(n, {arg i; f0*(i+1)});
	var amps = Array.fill(n, {arg i; 1/(i+1)});
	var filtered_amps = n.collect {arg i; if (DC.ar(freqs[i]) < MouseX.kr(1000, 2000), amps[i], 0)};
	var sig = Mix.ar(SinOsc.ar(freq:freqs, mul:filtered_amps));
	Pan2.ar(sig*0.5, 0.0);
}.play;
)

Some syntactic suggestions to save typing. ‘indicator’ is a multichannel signal providing 0s and 1s.

(
{
	var n = 30;
	var f0 = 200;
	var freqs = (1..n) * f0;
	var amps = 1 / (1..n);
	var indicator = freqs < MouseX.kr(1000, 2000);
	var sig = Mix.ar(SinOsc.ar(freq: freqs, mul: indicator * amps));
	Pan2.ar(sig * 0.5, 0.0);
}.play;
)

This approach is absolutely ok, can be extended to complex additive setups in multiple ways (using independant LFOs for amplitude and frequency, alternative frequency choices, array arguments, other UGens than SinOsc, feedback etc.).

Patterns are also a nice way to do additive synthesis. At the moment I’m working with some students doing additive synthesis projects. On this occasion I was remembered how well Patterns are suited for this. One big plus is that you can use the Event apparatus for defining harmonic relations. I wanted to collect some additive synthesis ideas in a tutorial, but didn’t have time to work it out yet. Here some Patterns examples:

Additive Synthesis with Patterns

// WARNING: note the low amp values (* 0.02)!
// needed as many single synths do overlap

// simple sine synthdef with spatial placement and trapezoidal envelope

(
SynthDef(\sine, { |out, freq = 400, att = 1, sus = 1, rel = 1,
	pan = 0, amp = 0.1|
	var env = EnvGen.ar(Env.linen(att, sus, rel, curve: \sine), 1, doneAction: 2);
	var sig = SinOsc.ar(freq, 0, amp) * env;
	OffsetOut.ar(out, Pan2.ar(sig, pan))
}).add
)

// moving clusters

(
p = Pbind(
	\instrument, \sine,
	\dur, 0.1,
	\att, 3,
	\sus, 3,
	\rel, 3,
	\amp, 0.02 * Pseg(Pwhite(0.0, 1), Pwhite(0.5, 1)),
	// Pseg generates env-like movement
	\midinote, Pseg(Pexprand(40.0, 110), Pwhite(0.2, 1.5), \sine),
	\pan, Pwhite(-1.0, 1)
).play
)

// needs some seconds to stop
p.stop



// microtonal clusters at random pitch, causing beats
(
p = Pbind(
	\instrument, \sine,
	\dur, 0.5,
	\att, 3,
	\sus, 3,
	\rel, 3,
	\amp, 0.02 * Pwhite(0.5, 1),
	\midinote, Pexprand(40.0, 110) + [0, 0.25, 0.5],
	\pan, Pwhite(-1.0, 1)
).play
)

// needs some seconds to stop
p.stop



// partial row glitter
(
p = Pbind(
	\instrument, \sine,
	\dur, 0.1,
	\att, 1,
	\sus, 2,
	\rel, 1,
	\amp, 0.02 * Pseg(Pwhite(0.0, 1), Pwhite(0.5, 1)),
	// fundamental walks with mediant steps, added quartertones
	\midinote, Pstutter(Pwhite(50, 100), Pseq([25, 29, 33], inf) + Pseq([0, 0.5], inf)),
	\harmonic, Pwhite(1, 30),
	\pan, Pwhite(-1.0, 1)
).play
)

// needs some seconds to stop
p.stop
6 Likes

… btw the DC here can be dropped, to implement a binary operator ugen with ‘<’ you need just one control input, which, in this case, is MouseX.kr

1 Like

Thanks so much for these resourceful answers, and also these syntactic suggestions and further examples! Very much appreciated!

Perhaps one of you also has a suggestions for how to best choose a value for n? I want to have harmonics all the way to around 15kHz, however, when I use a fixed number I easily run into aliasing problems when I change my fundamental frequency. Therefore, I tried something like this:

var f0 = MouseY.kr(50 ,2000);
var n = floor(15000/f0).asInteger;

But that does not seem to work. Any ideas?

This doesn’t happen in the given example. Amplitudes of frequencies above the threshold are set to 0.
Moreover the graph of a Synthdef is always fixed, so it’s impossible to make the number of parallel SinOscs variable. What you can do, however, is define a maximum number and go up to this. This is similar to the the built-in frequency threshold already done here. So take a sufficiently high max number of partials and set some to amplitude 0, by a maximum frequency and/or different criteria (e.g. blockwise muting is interesting).

(
// see freqscope with linear scaling
{
	var f0 = MouseY.kr(50, 2000);
	var n = 200;
	var freqs = (1..n) * f0;
	var amps = 0.5 / (1..n) ** 2;
	var indicator = freqs < MouseX.kr(100, 20000);
	var sig = Mix.ar(SinOsc.ar(freq: freqs, mul: indicator * amps));
	Pan2.ar(sig * 0.5, 0.0);
}.freqscope;
)
1 Like

@dkmayer your approach works exactly how I wanted it! Many thanks!
I guess my attempt was a bit too complicated.