Week 5, lesson 3 - adding and using superdirt synths

I was taking a look at the default synths in SuperDirt and noticed that they all have an EnvGen - how come if this may cause interference with the way SuperDirt manages envelopes?

In that case, I'm probably wrong on this point.. @julian do you have a moment to explain how the envelopes in superdirt synths should behave, when (unless I misunderstand completely) superdirt itself applies an envelope to its synths?

1 Like

Yes please :pray:

And, if possible, I would love to have some insight on the synth design.
For instance, I see in that file that EnvGens often have doneAction: 2 and I am trying to understand if this is:

  • a sort of mapping between events in Tidal and events in SC, i.e. you create a synth for each event and kill it after the event has played so you keep kind of 1:1 correspondence with what SC receives from Tidal
  • some good design practice in SC
  • a combination of both points

It would be so interesting seeing Tidal on the SuperCollider side!

@mattia.paterna I just published the supplementary video on creating synths for TidalCycles. It incidentally addresses some of the stuff you've inquired about here. Please let me know if there's anything you'd like to see covered in a bit more depth, or if there's anything important I've neglected to explain.

22 Likes

Thanks a lot @eris I'm going to learn a lot from this !!

1 Like

Ok, here you go!

The following concerns only synths that come from the tidal "sound" function (not global effect synths like # delay, that are handled differently).

  1. In superdirt, the freeing of synths is done by one internal synth that makes the end of the chain of effects. It is the dirt_gate synth. Its definition is in core-synths.scd. I posted it below [1]. It applies a minimal envelope to the whole event (including all the effects you applied to it). The doneAction: 14 is called after this envelope is completed. By setting the fadeInTime and fadeTime parameters in tidal, you can harden or soften the attack/decay.
    This means that you can make SynthDefs with synths that do not release themselves, something we normally avoid in supercollider, because you'd pile up synths endlessly. But here, you could simply define a synth like this:
    SynthDef(\mess, { |out| Out.ar(out, GrayNoise.ar) }).add
    and you could play it in tidal with: sound "mess". The synths are freed by the dirt_gate synth.

  2. But usually, you want a synth to have a particular amplitude envelope. Then you can define one in your SynthDef, see below [2]. Note that sustain means the duration of the whole synth (this is what it is called in SuperCollider in general), which is sent over from tidal (for setting it directly, try # sustain "0.1 0.3 0.5 1"). Then you can have a doneAction: 2 if you like (this will free the synth after the envelope is done), but you can also leave this doneAction out altogether, because the synth is freed externally anyhow. For example: SynthDef(\mess, { |out, sustain = 0.2| Out.ar(out, GrayNoise.ar * XLine.kr(1, 0.001, sustain)) }).add
    But sometimes, you may want to use the synth outside superdirt, and then it is polite that it cleans up after itself. Just make sure that you multiply your envelope with the audible signal, otherwise you'll hear clicks at the end of each synth.
    SynthDef(\mess, { |out, sustain = 0.2| Out.ar(out, GrayNoise.ar * XLine.kr(1, 0.001, sustain, doneAction: 2)) }).add

Hope this helps!

[1]

SynthDef("dirt_gate" ++ numChannels, { |out, in, sustain = 1, fadeInTime = 0.001, fadeTime = 0.001, amp = 1|
		var signal = In.ar(in, numChannels);
		 //  doneAction: 14: free surrounding group and all nodes
		var env = EnvGen.ar(Env([0, 1, 1, 0], [fadeInTime, sustain, fadeTime], \sin), levelScale: amp, doneAction: 14);
		signal = signal * env * DirtGateCutGroup.ar(fadeTime, doneAction: 14);
		OffsetOut.ar(out, signal);
		ReplaceOut.ar(in, Silent.ar(numChannels)) // clears bus signal for subsequent synths
	}, [\ir, \ir, \ir, \ir, \ir, \ir]).add;

[2]

(
SynthDef(\imp, { |out, sustain = 1, freq = 440, speed = 1, begin=0, end=1, pan, accelerate, offset|
	var env, sound, rate, phase;
	env = EnvGen.ar(Env.perc(0.01, 0.99, 1, -1), timeScale:sustain, doneAction:2);
	phase = Line.kr(begin, end, sustain);
	rate = (begin + 1) * (speed + Sweep.kr(1, accelerate));
	sound = Blip.ar(rate.linexp(0, 1, 1, freq) * [1, 1.25, 1.51, 1.42], ExpRand(80, 118) * phase).sum;
	OffsetOut.ar(out,
		DirtPan.ar(sound, ~dirt.numChannels, pan, env)
	)
}).add
);
7 Likes

I just saw the great explanation of @eris – thank you! I should have waited a little longer :wink:

1 Like

@alex: note that the synths you showed in the video have an envelope that has a release that is separate from the sustain, so it will be cut off early, I think. This is a problem of the word "sustain" because it is used in different ways. But it may be that it works accidentally for some tricky reason (not sure right now).

I tend to use sustain as a time scale for the envelope, e.g.:

EnvGen.ar(Env.perc(0.01, 0.99, 1, -1), timeScale:sustain, doneAction:2);
2 Likes

this is very cool! I am excited to start making some synths, and interested if there are any simple ways to make the synths reactive/interactive with the environment. I would love to make a generative dance floor where people, lights and visuals have a way to effect the sounds being played. Is it possible to make a synth that changes with mic input or webcam?

4 Likes

That is so much valuable information!
Thanks @julian for your in-depth explanation of how things work on SC side, and thanks @eris for the super inspiring video.
I really look forward to having more of these discussions!

2 Likes

A mic input is just the SoundIn UGen. You can just use it. There are also default synths that have mic input, have a look there. If you want to send data to superdirt, you could make a synth event explicitly like this:

~dirt.soundLibrary.addSynth(\yourSynth, (instrument: \yourSynthDefName, specialParameter: 5));

and then from the function that receives your external data (OSCdef, MIDIdef, or SerialPort) you can just set that parameter.


~dirt.soundLibrary.addSynth(\yourSynth, (instrument: \yourSynthDefName, specialParameter: 5));

~dirt.soundLibrary.set(\yourSynth, 0, \specialParameter, 9); // the zero means the first event (you can add several, because tidal sends an `n` parameter that lets you select which).

~dirt.soundLibrary.at(\yourSynth)
6 Likes

thank you
this is super-fantastic :slight_smile:
mucho info, very well presented

Great video thanks! Also nice unified tidal + supercollider setup in atom!

1 Like

Really well laid out and well explained. Thank you! I can see how this is going to be useful.

2 Likes

awzm!! thanks!

1 Like

Hello everybody,
if it can help someone, I made a function to list all the parameters from your SynthDefs and declare them in Tidal.
This function actually generate the code in the SuperCollider Post window, you have to copy/paste the code in your bootTidal.hs (with the "let" or without depend on your needs, feel free to remove it).
In the first place I use to do this with SuperDirt.postTidalParameters(aSynthDefList);
but the code generate by this isn't usable directly in bootTidal.hs for some reasons (maybe parenthesis), and the list of predefined parameters (parameters exclude for avoid collisions) is not complete:
#[\pan, \amp, \out, \i_out, \sustain, \gate, \accelerate, \gain, \overgain, \unit, \cut, \octave, \offset, \attack];
so I used another one.
Execute this in SuperCollider:

~tidalScopeTotalCodeGen = { arg key = "", targetSearch = 2; // look for SynthDef names which contain key at position define by targetSearch and generate the code to declare args in tidal
var code, presentCtrl/*, targetSynths*/;
code = "let ";
presentCtrl = List.new;
// targetSynths = List.new;
key = key.asString;

SynthDescLib.global.synthDescs.do({ arg item;
	var name, nameSize, keySize, start, end;
	
	name = item.name.asString;
	nameSize = item.name.size;
	keySize = key.size;
	
	switch(targetSearch,
		0, { // search at the begining
			start = 0;
			end = keySize;
		},
		1, { // search everywhere
			start = 0;
			end = nameSize;
		},
		2, { // search at the end
			start = nameSize - keySize;
			end = nameSize;
		},
		{ // default function search at the end
			start = nameSize - keySize;
			end = nameSize;
		}
	);
	
	if(key.matchRegexp(name, start, end), {
		
		item.controls.do{ arg control;
			var controlName, controlNameLower;
			controlName = control.name.asString;
			controlNameLower = controlName.toLower;
			
			if ((#["?", "slide", "speed", "spread", "legato", "octave", "unit", "accelerate", "loop", "offset", "nudge", "lfo", "rate", "size", "room", "dry", "cut", "cutoff", "resonance", "n", "freq", "note", "degree", "harmonic", "delay", "pan", "gain", "overgain", "lpf", "hpf", "attack", "att", "decay", "sustain", "hold", "sus", "release", "rel", "span", "out", "i_out", "in", "i_in", "input", "output", "inbus", "outbus", "doneaction", "done", "gate", "t_gate", "trig", "t_trig"].includesEqual(controlNameLower).not and: presentCtrl.includesEqual(controlName).not), {
				code = code ++ controlName ++ " = pF \"" ++ controlName ++ "\"\n    ";
				presentCtrl.add(controlName);
			});
			
		};
		
		// targetSynths.add(name.asSymbol);
		
	});
	
});

// SuperDirt.postTidalParameters(targetSynths);

Post << code;

};

~tidalScopeTotalCodeGen.value;

5 Likes

You can use the key parameter to look for a special key in SynthDefs names.
I use this to only targeting SynthDefs made specifically for SuperDirt, I suffix their names with Sd so I execute the function like this:
~tidalScopeTotalCodeGen.value("Sd", 2);
2 is for suffix
0 for prefix
1 look everywhere
Hope it helps

1 Like

There is a convenience function in superdirt that posts the code for adding SynthDef parameters in tidal

SuperDirt.postTidalParameters(synthNames: <a list of synth def names>);

// for example
SuperDirt.postTidalParameters(synthNames: [\supersaw, \supermandolin]);

// posts:

-- | parameters for the SynthDefs: supersaw, supermandolin
let (decay, decay_p) = pF "decay" (Nothing)
    (detune, detune_p) = pF "detune" (Nothing)
    (freq, freq_p) = pF "freq" (Nothing)
    (lfo, lfo_p) = pF "lfo" (Nothing)
    (pitch1, pitch1_p) = pF "pitch1" (Nothing)
    (rate, rate_p) = pF "rate" (Nothing)
    (resonance, resonance_p) = pF "resonance" (Nothing)
    (semitone, semitone_p) = pF "semitone" (Nothing)
    (span, span_p) = pF "span" (Nothing)
    (voice, voice_p) = pF "voice" (Nothing)



Is this of any help?

7 Likes

thank you

Great video, thanks.

Why do you use IEnvGen, instead of the more commonly used EnvGen?
The 'I' stands for interactive?