Envelope not working and not being duplicated

Hello,

I am very beginner here, don’t have much music theory and because I fail in distinguish oscillators I am trying to replicate an exercise described in this Ear Training for Sound Design video in SuperCollider. It’s about playing a random wave and guessing by ear. This is what I arrived to:

(
SynthDef(\sine, { |out = 0, freq = 440, amp = 0.1|
    Out.ar(out, SinOsc.ar(freq, 0, amp))
}).add;

SynthDef(\saw, { |out = 0, freq = 440, amp = 0.1|
    Out.ar(out, Saw.ar(freq, amp))
}).add;

SynthDef(\tri, { |out = 0, freq = 440, amp = 0.1|
    Out.ar(out, LFTri.ar(freq, 0.5, amp))
}).add;

SynthDef(\pulse, { |out = 0, freq = 440, amp = 0.1|
    Out.ar(out, Pulse.ar(freq, 0.5, amp))
}).add;

~synthDefs = [\sine, \saw, \tri, \pulse];
)

(
SynthDef(\env, { |in, out = 0|
  var sig, env;
  sig = In.ar(in, 1);
  env = EnvGen.kr(Env([0, 1, 0], [1, 0.1]), \doneAction: 2);
  sig = sig * env;
  sig = sig ! 2;
  Out.ar(out, sig);
}).add;
)

(
~randomSynth = {
  Synth(\env, [\in, 1]);
  Synth(~synthDefs.choose, [
    \freq, (60 + Scale.major.semitones).midicps.choose,
    \amp, rrand(0.1, 0.4),
    \out, 1,
  ]);
};
)

~randomSynth.();

Here I have four types of oscillators and I want to play a random one each time I execute the function. Because I only want them to last for one second, I am sending their output to an envelope that is supposed to free after use. Also I would like to be stereo.

I have both problems, they are not freed, and they are not in stereo. I somehow listen the synth for one second in my left ear, but on the right is keep going. This makes me think that I am applying the envelope only to the left signal, but in the code I am duplicating the result signal * envelope afterwards.

Could you point out what is that have incorrect please?

Also interested in know how would you do code this exercise in SuperCollider.

Thanks,
Juan

Hi Juan,

I’ve slightly modified your env SynthDef, so not only the instance of this SynthDef gets freed (doneAction: 7 instead of doneAction: 2 - “free this synth and all preceding nodes in this group” - see https://doc.sccode.org/Classes/Done.html#Actions because actually the env Synth was freed correctly but the Synth that fed into it (sine, saw, tri or pulse) wasn’t.
Also fixed the routing, making sure that the output of the sound-generating Synth is really fed into the env Synth by sending the output of the sound-generating Synths to a dedicated bus (the output wouldn’t be audible without being routed to the env Synth. Only env plays its output to the audible bus 0). Also made sure the env is always put at the end of the chain, using tail instead of the implicit new method when creating the env Synth and using head for the sound-generating Synths which should guarantee the right order on the server:

(
SynthDef(\env, { |in, out = 0|
	var sig, env;
	sig = In.ar(in, 1);
	env = EnvGen.kr(Env([0, 1, 0], [1, 0.1]), doneAction: 7);
	sig = sig * env;
	sig = sig ! 2;
	Out.ar(out, sig);
}).add;
)

(
~randomSynth = {
	var bus = Bus.audio(s);
	Synth.tail(s, \env, [\in, bus]);
	Synth.head(s, ~synthDefs.choose, [
		\freq, (60 + Scale.major.semitones).midicps.choose,
		\amp, rrand(0.1, 0.4),
		\out, bus,
	])
}
)

Hope that makes it clearer.

Stefan

Another way would be to put an envelope into each waveform synth:

(
SynthDef(\sine, { |out = 0, freq = 440, amp = 0.1|
	var sig, env;
	sig = SinOsc.ar(freq, 0, amp);
	env = EnvGen.kr(Env([0, 1, 0], [1, 0.1]), doneAction: 2);
	sig = sig * env;
    Out.ar(out, sig ! 2)
}).add;

SynthDef(\saw, { |out = 0, freq = 440, amp = 0.1|
	var sig, env;
	sig = Saw.ar(freq, amp);
	env = EnvGen.kr(Env([0, 1, 0], [1, 0.1]), doneAction: 2);
	sig = sig * env;
    Out.ar(out, sig ! 2)
}).add;

SynthDef(\tri, { |out = 0, freq = 440, amp = 0.1|
	var sig, env;
	sig = LFTri.ar(freq, 0.5, amp);
	env = EnvGen.kr(Env([0, 1, 0], [1, 0.1]), doneAction: 2);
	sig = sig * env;
    Out.ar(out, sig ! 2)
}).add;

SynthDef(\pulse, { |out = 0, freq = 440, amp = 0.1|
	var sig, env;
	sig = Pulse.ar(freq, 0.5, amp);
	env = EnvGen.kr(Env([0, 1, 0], [1, 0.1]), doneAction: 2);
	sig = sig * env;
    Out.ar(out, sig ! 2)
}).add;

~synthDefs = [\sine, \saw, \tri, \pulse];
)

(
~randomSynth = {
  Synth(~synthDefs.choose, [
    \freq, (60 + Scale.major.semitones).midicps.choose,
    \amp, rrand(0.1, 0.4),
    \out, 0,
  ]);
};
)

~randomSynth.();

Thank you Stefan for the explanation, I now see that the output 1 I was using was the right speaker, and so I have to pick some higher number to do the connection manually.

I found out that when two synth were running at the same time, they all ended at the same time. So I group them and change doneAction to 14 to free each group individually.

(
SynthDef(\env, { |in, out = 0|
	var sig, env;
	sig = In.ar(in, 1);
	env = EnvGen.kr(Env([0, 1, 0], [1, 0.1]), doneAction: 14);
	sig = sig * env;
	sig = sig ! 2;
	Out.ar(out, sig);
}).add;
)

(
~randomWave = {
	var bus = Bus.audio(s),
  group = Group.tail(s.defaultGroup);
	Synth.tail(group, \env, [\in, bus]);
	Synth.head(group, ~waves.choose, [
		\freq, (60 + Scale.major.semitones).midicps.choose,
		\amp, rrand(0.1, 0.4),
		\out, bus,
	])
}
)

I also thought about putting each envelope in each wave, but I do want to be able to change their length and in one place seems more elegant, well… another arg in each synth can be easily be given too.

As the exercise is without looking I think I would make this length value change with a key or maybe with the mouse (but is more difficult to keep the count on where it is).

Ah, right, using a Group and freeing that on finish is an even better solution (the group doesn’t necessarily have to be be added to the tail of the default group but the synths within the group have to be in order).

One thing that came to my mind after I had sent off my post was, that for each execution of your ~randomSynth, respectively ~randomWave function bus = Bus.audio(s) creates a new bus. This could be avoided by assigning the bus like this:

bus = s.options.firstPrivateBus

… so it would always be the same bus your waves would be played to. But since you seem to play more than one wave at the same time that wouldn’t work anymore.

Actually I was trying to make them play one after the other, with a pause in the middle, in groups of three. I think I made it work for a moment, but like this it just plays once.

(
~guessWaves = {
  Routine({
    3.do({ |i|
      var bus, group, len;
      (i + 1).post; " ".post;
      bus = s.options.firstPrivateBus;
      group = Group(s.defaultGroup);
      len = 2; // MouseX.kr(0.5, 7, 0, 0.2).round(0.25);
      Synth.tail(group, \env, [
        \in, bus,
        \len, len,
      ]);
      Synth.head(group, ~waves.choose.postln, [
        \freq, (60 + Scale.major.semitones).midicps.choose,
        \amp, rrand(0.1, 0.4),
        \out, bus,
      ]);
      1.wait;
    });
  });
};
)

~guessWaves.play;

And it leaves something in the default group that is never freed. I suspect is the routine itself, but I could not make it go away.

No, a Routine is strictly language-side. You’re not calling play on the routine but on the function. That has a special meaning: {}.play creates a temporary SynthDef (temp_XX) and plays an instance on the server immediately. But you’re not playing the routine (well, ~guessWaves.play indeed creates one synth but only one). To play the routine as intended you must call it like this:

~guessWave.().play; // execute the function and play the returned routine

To summarize, for this problem, there are two main approaches:

  1. Put the oscillator and the envelope into one SynthDef (one SynthDef per oscillator type).
    • Pro: Playing a note is simpler (just play one synth).
    • Con: Code duplication as in TXMod’s example, or more surface-level code complexity to mass-produce SynthDefs (see also example below).
  2. Shared logic in one SynthDef, plus a separate SynthDef per audio source, connected by buses.
    • Pro: SynthDefs are minimally simple.
    • Con: Managing nodes and buses can be tricky.

Which one is more complex depends on, for instance, how many SynthDefs are involved and the context in which they are played. Here, by approach #1, 4 audio sources times 1 envelope synth = 4 SynthDefs, where approach #2 needs 5 SynthDefs and more complex play logic. So there is not really a good reason in this specific case to continue with buses. If, on the other hand, you have 10 sources and 20 effect synths, #1 needs 200 SynthDefs where #2 needs 30 SynthDefs – those buses start to look more attractive. If you’re playing in a Routine, then you have total control over the playback logic; if you’re using patterns, then you would need to make an event type, making #2 a bit heavier.

For 4 oscillators, I’d just fold them in (demonstrating the mass-production of SynthDefs here):

(
// here, define what is different between the several synthdefs
// assigning the dictionary to a var here is mainly for IDE indentation :-\
var defs = (
	sine: { |freq = 440|
		SinOsc.ar(freq, 0)
	},
	saw: { |freq = 440|
		Saw.ar(freq)
	},
	tri: { |freq = 440|
		LFTri.ar(freq, 0.5)
	},
	pulse: { |freq = 440|
		Pulse.ar(freq, 0.5)
	}
);

~synthDefs = defs.keys;

defs.keysValuesDo { |name, oscil|
	// common logic (enveloping, amp scaling) is written only once
	SynthDef(name, { |out = 0, amp = 0.1|
		var sig, env;
		sig = SynthDef.wrap(oscil);
		env = EnvGen.kr(Env([0, 1, 0], [1, 0.1]), doneAction: 2);
		sig = sig * (env * amp);
		sig = sig ! 2;
		Out.ar(out, sig);
	}).add;
};

~randomSynth = {
	Synth(~synthDefs.choose, [
		\freq, (60 + Scale.major.semitones).midicps.choose,
		\amp, rrand(0.1, 0.4),
	]);
};

But… #2 would look a lot more attractive if somebody had designed a class that handles the bus routing and resource management automatically.

Oh, wait… I did: GitHub - jamshark70/ddwPlug: SuperCollider dynamic per-note synth patchingQuarks.install("https://github.com/jamshark70/ddwPlug") if you have git installed.

Then:

(
SynthDef(\sine, { |out = 0, freq = 440, amp = 0.1|
	Out.ar(out, SinOsc.ar(freq, 0, amp))
}).add;

SynthDef(\saw, { |out = 0, freq = 440, amp = 0.1|
	Out.ar(out, Saw.ar(freq, amp))
}).add;

SynthDef(\tri, { |out = 0, freq = 440, amp = 0.1|
	Out.ar(out, LFTri.ar(freq, 0.5, amp))
}).add;

SynthDef(\pulse, { |out = 0, freq = 440, amp = 0.1|
	Out.ar(out, Pulse.ar(freq, 0.5, amp))
}).add;

~synthDefs = [\sine, \saw, \tri, \pulse];

SynthDef(\env, { |out = 0|
	var sig, env;
	// note, input is a real audio-rate control
	// not a bus number
	sig = NamedControl.ar(\in, 0);
	env = EnvGen.kr(Env([0, 1, 0], [1, 0.1]), doneAction: 2);
	sig = sig * env;
	sig = sig ! 2;
	Out.ar(out, sig);
}).add;
)

(
~randomSynth = {
	Syn(\env, [
		in: Plug(~synthDefs.choose, [
			\freq, (60 + Scale.major.semitones).midicps.choose,
			\amp, rrand(0.1, 0.4)
		])
	]);
};
)

… literally plugging the oscillator synth into the input. (I designed this quark initially for control rate modulation, but it does support audio rate patching too.)

  • Pro: Simple synthdefs and easy patching.
  • Con: Creating a Syn is a more complex operation than creating a Synth (i.e. some CPU overhead in the language), but this is negligible at typical note rates. (I wouldn’t do one-grain-one-synth style granular processing this way.)

hjh

The Select UGen might be relevant to know about as well:

(
SynthDef(\earTrain,{
    var freq = \freq.kr(440);
    var wave = [
        SinOsc.ar(freq),
        Saw.ar(freq),
        LFTri.ar(freq, 0.5),
        Pulse.ar(freq, 0.5)
    ];
    var sig = Select.ar(\which.kr(0), wave);
    sig = sig * Env([0, 1, 0], [1, 0.1]).ar(2);
    sig = sig ! 2 * \amp.kr(1);
    Out.ar(\out.kr(0), sig)
}).add
);

// loop groups of three with a pause in between:
(
Routine({
    var pause = 0.5;

    loop {
        3.do{
            var index = 4.rand;
            "wave index: %".format(index).postln;
            Synth(\earTrain,[
                \which, index, 
                \freq, (60 + Scale.major.semitones).midicps.choose,
                \amp, rrand(0.1, 0.4),
            ]);
            (1.1 + pause).wait
        };
        pause.wait
    }
}).play
);

@guide42 you mentioned earlier about wanting to change the envelope length; here you can simply add some new arguments to one SynthDef and update the Routine as desired. :slight_smile:

Lots things to look into.

I like the factory pattern to create SynthDef, is a nice way of tempting a behavior that can have many base waves and with different names. I can imagine that having them combined would be easier for the CPU. I recently moved to linux-rt (I am using Arch) because of SuperCollider warning about not being able to set priorities. I haven’t had any issue yet, but I am only doing very simple things.

Although the bus plug way is very nice. Having to remember numbers, even if simplified by Bus, it has a kind of nostalgic feeling, and might be more interesting to do it “the hard way” just for performance. Not even close of this, just trying to “feel” this instrument, and the “plug” analogy is very good.

As for the Select example, it lacks something important that is to post the answer. I might hide the post window or just cover it with my hand during guessing, but afterwards I’d like to know which one was which one. (BTW, I think I already improved in this guessing the wave :slight_smile:)

For that I tried to use the dictionary in the previous example but I fail extracting the function or wrapping the SynthDef. I have:

// ~waves is jamshark70's defs
(
SynthDef(\earTrain, { |which|
  var freq = \freq.kr(440);
  var sig = SynthDef.wrap(~waves.at(which));
  sig = sig * Env([0, 1, 0], [1, 0.1]).ar(2);
  sig = sig ! 2 * \amp.kr(1);
  Out.ar(\out.kr(0), sig)
}).add;
)

(
Routine({
  var pause = 5;
  3.do{ |index|
    var which = ~waves.keys.choose;
    "% %".format(index, which).postln;
    Synth(\earTrain,[
      \which, which,
      \freq, (60 + Scale.major.semitones).midicps.choose,
      \amp, rrand(0.1, 0.4),
    ]);
    pause.wait;
  };
}).play;
)

How is it that from a simple exercise so may ways can be done. Beside being a personal preference, some things like code repetition are more difficult to change, and many of the examples are quite verbose. This last one, with only one \earTrain synth seems to be easiest case. Wonder how I got into bus switching from the start. Thanks you all.