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?
Yes please
And, if possible, I would love to have some insight on the synth design.
For instance, I see in that file that EnvGen
s 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.
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).
-
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 incore-synths.scd
. I posted it below [1]. It applies a minimal envelope to the whole event (including all the effects you applied to it). ThedoneAction: 14
is called after this envelope is completed. By setting thefadeInTime
andfadeTime
parameters in tidal, you can harden or soften the attack/decay.
This means that you can makeSynthDef
s 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 thedirt_gate
synth. -
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 adoneAction: 2
if you like (this will free the synth after the envelope is done), but you can also leave thisdoneAction
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
);
@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);
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?
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!
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)
thank you
this is super-fantastic
mucho info, very well presented
Great video thanks! Also nice unified tidal + supercollider setup in atom!
Really well laid out and well explained. Thank you! I can see how this is going to be useful.
awzm!! thanks!
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;
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
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?
thank you
Great video, thanks.
Why do you use IEnvGen, instead of the more commonly used EnvGen?
The 'I' stands for interactive?