Patterns with fixed dur Envelopes

Or to be clearer, if you explicitly define it I think converting it would be surprising. How it’s used in the synthdef is another issue.

I assume with a gated envelope sustain would behave as in beats.

FWIW the approach that I’ve taken in my own SynthDefs is to use sustain only for gate control (in beats), and, if I have to pass a duration for a timed envelope, to calculate it explicitly as \time, Psomething(...) / Ptempo() (the latter in the ddwPatterns quark). This has the disadvantage of needing to be written out every time (though… it’s really not so inconvenient), but the advantage of depending on no magic and thus being unbreakable.

hjh

I would worry that changing it now to be relative to tempo may break something? (since it is in seconds right now)

Okay, I think we need to be clear. It’s not in seconds, it’s in beats. So comparing the difference in behaviour between fix dur and gated envs:

SynthDef(\x, { |sustain| Out.ar(0, Env.linen(0.01, sustain.poll(0, "sustain"), 0.1).kr(2) * Blip.ar(Rand(200, 250), 20) * 0.1) }).add;

SynthDef(\xgate, { |gate = 1| Out.ar(0, Env.asr(0.01, releaseTime: 0.1).kr(2, gate) * Blip.ar(Rand(200, 250), 20) * 0.1) }).add;

Pbind(\instrument, \x, \dur, 0.25, \sustain, 0.25).play;
TempoClock.default.tempo = 1; // -> 0.25
TempoClock.default.tempo = 2; // -> 0.25

Pbind(\instrument, \xgate, \dur, 0.25, \sustain, 0.25).play;
TempoClock.default.tempo = 1; // -> 0.25
TempoClock.default.tempo = 2; // -> 0.125 seconds in effect, so beats

It is possible of course that people have misunderstood how it works and thought it was passing seconds to the synth. I certainly did!

I don’t propose we change the current behaviour of sustain. If somebody did misunderstand it will still work the same way at least. I also think it would be different than the existing conversions, which provide equivalent values in different units, eg midinote->freq. I think converting the units of a standard key in some cases (sustain = beats → sustain = seconds) would be confusing. Just let it always be in beats.

I suggest we add a new sustainAbs standard key with auto conversion, and probably also tempo.

I see – this makes it much clearer now. I would never ever have expected that a sustain argument actually can be used in a gated synth. And in fact it is in some way not entirely consistent to use sustain in a gated synth: when the tempo changes during the synth runs, the value of sustain is not changed accordingly.

[…]

Just so I understand: this would change the behaviour under tempo changes of the following:

Pbind(\instrument, \x, \dur, 0.25, \sustain, 0.25).play;

Well, it was more for the sake of demonstration, but I can see a case for specifying it directly rather than by legato. I’ve never been convinced that percentage of sustain across varying durations of notes models any common musical behaviour. (But it’s not used directly in the synth in the example above.)

Um, can you clarify what you mean by ‘this’ and how the behaviour would change? Currently when you change the tempo the sustain time in seconds changes, but is fixed to 0.25 of a beat. What I’m proposing would not change that.

In the case of specifying the behaviour of a gated envelope, sustain must be in beats, yes. Legato is a specific thing.

So we are talking about two different uses of sustain:

  1. by the clock for scheduling the end of the synth
  2. by the synth for determining its own end

The clock knows its own tempo, so there is no need to do anything about sustain to make it work in beats. The synth has no idea about a tempo, so if a synth needs to be tempo sensitive, it would either

  • implicitly receive durational values like sustain converted into beats
  • explicitly by passing tempo to the synth

In both cases, there is no (easy) update when tempo changes at the runtime of the synth. I think it may be nice if gated and ungated Synths would work the same. But this is a change that may have an impact on existing code, isn’t it?

In the case of a SynthDef with a fixed duration envelope, the sustain time in the synth does not change:

It would if we adjusted it.

I am sorry, I don’t want to make things complicated – maybe I just don’t get something very simple.

My understanding was that sustain was meant to be used in a gated synth, because it determines the scheduling delta for the note release. This is why, when I wrote timed-envelope synthdefs, I chose a different name for the time in seconds! It struck me that it would be a source of confusion to use sustain for a time in seconds (as a synth input).

hjh

1 Like

So we have a specification problem, I suppose?

1 Like

SC in general doesn’t do specs, and this leads to no end of trouble :laughing:

I guess my read of it is that sustain has one meaning (“release after x beats”) that is explicitly defined in the Event codebase, and a potential second meaning that arises accidentally from the two facts that a/ values in an event are transmitted to control inputs of the same name and b/ the server doesn’t know the client’s tempo without being told, so a transmitted sustain value would be assumed in the server to be seconds.

The first (beats until release), you can point to specific lines of code that determine this behavior. The second… well, of course there are lines of code that define it (in the sense that nothing happens in a computer without lines of code), but they are rather distant from the place where the Event is being processed. So to me, the “seconds” interpretation feels indirect and vague, where the first is literally Right There.

My opinion places a higher priority on the meaning that can be readily inferred from reading the Event code. “But it also just happens to xyz,” I don’t find terribly compelling, tbh.

I’d be just as happy if the documentation simply recommended to avoid using sustain as a control input name, and explain the conflict.

hjh

I think the problem is we need to be very precise in our language! :slight_smile: Yes, the sustain value sent to the synth is the same. What it should mean of course is another matter. I’ve proceeded from the point that it means ‘sustain time in beats’. That’s consistent with the doc and existing behaviour both in events and values passed to synth controls.

Yes, if you want to use it in a synth correctly, you would need to know the tempo, and it would be nice if that was supplied implicitly.

I’d suggest keeping that as a slightly separate issue. I imagine that may be tricky to do, and could break things, yes. It might well be reasonable to say you have to use gated envs if you want that behaviour.

So I think the desirables are:

  1. Provide a standard key for sustain in seconds, maybe called sustainAbs.
  2. Provide tempo to synths implicitly, i.e. whether or not the event explicitly sets it.
  3. Update sustain for tempo changes in running synths with fixed dur envs.

1 and 2 seem straightforward and I think we should at least do those.

1 Like

Again, I think sustain and sustainAbs would make this very clear, and consistent with taxonomy elsewhere.

I think you’re looking for the Tuning class. There is support for non 12tet tunings and scales. But if you want to discuss this further, please make a separate post. This thread is already very long and involves a few issues and I don’t want this to get lost.

OK, I do see your point. Of course, this means that all current SynthDefs with fixed duration envelope are incorrectly written …

To correct them, we need to write
EnvGen.kr(env, timeScale:1/\tempo.kr(1))
… or use sustainAbs.

Sure, no problem with that (and it implicitly avoids using sustain as a control input name :wink: ).

I have fixed-duration envelopes dating back to 2003 or 2004 that are correctly written, because I didn’t call the duration parameter “sustain.”

hjh

This is the most minimal change I can imagine that would mostly solve this problem:

diff --git a/SCClassLibrary/Common/Collections/Event.sc b/SCClassLibrary/Common/Collections/Event.sc
index 837c16f3c..505012939 100644
--- a/SCClassLibrary/Common/Collections/Event.sc
+++ b/SCClassLibrary/Common/Collections/Event.sc
@@ -243,11 +243,12 @@ Event : Environment {
 			),
 
 			durEvent: (
-				tempo: nil,
+				tempo: { thisThread.clock.tempo },
 				dur: 1.0,
 				stretch: 1.0,
 				legato: 0.8,
 				sustain: #{ ~dur * ~legato * ~stretch },
+				sustainSeconds: #{ ~sustain.value / ~tempo.value },
 				lag: 0.0,
 				strum: 0.0,
 				strumEndsTogether: false
@@ -447,10 +448,11 @@ Event : Environment {
 					server = ~server = ~server ? Server.default;
 
 					~finish.value(currentEnvironment);
-
-					tempo = ~tempo;
-					tempo !? { thisThread.clock.tempo = tempo };
-
+					
+					tempo = ~tempo.value;
+					if (tempo != thisThread.clock.tempo) {
+						thisThread.clock.tempo = tempo;
+					}
 
 					if(currentEnvironment.isRest.not) {
 						eventTypes = ~eventTypes;

Effectively, adding:

// durEvent
	tempo: { thisThread.clock.tempo },
	sustainSeconds: #{ ~sustain.value / ~tempo.value },

// play function
	tempo = ~tempo.value;
	if (tempo != thisThread.clock.tempo) {
		thisThread.clock.tempo = tempo;
	}

I believe this solves all of the most immediate problems, with no breakages to existing code except in VERY obscure scenarios (e.g. user code that nil-checks tempo). I see one downside - sustainSeconds behavior is slightly weird if you try to SET this key in your event, since it does not change anything about the sustain behavior at the event level, only (potentially) passes a different value to the Synth.

A proper solution for this is quite complicated, since we don’t really have a model for event keys that are read-only computed values - I tried to mock this up, and tbh it feels like a can of worms. I think something LIKE my change is acceptable as long as the behavior of the new key is documented (preferably as something like… a “computed” key that doesn’t change behavior).

I mocked up a few ideas for this, but ultimately it is - at BEST - very complex and fragile. This could be done in a relatively clean way as an add-on to the Event system, but since it requires Synth lifetime tracking that the event system doesn’t already do, it would really be better as a separate module vs building it in deeply.

If this gets merged in, here is the reminder that adding new keys to Event types is technically a breaking change, in the low but nonzero chance that someone is using it as a control name. There may be someone out there who specifically made a sustainSeconds parameter to get around this exact issue. (Changing interactions with other keys can also impact downstream event types derived by the user iirc? But I haven’t tracked this closely enough to determine whether this is the case here.) It’s somewhat unlikely, but plausible enough that a note needs to go in the changelog.

If in doubt about the sustainSeconds, I think that it would also be a reasonable approach to include tempo in the relevant SynthDefs and divide any time values explicitly there. This would have the advantage of being clear and general. Also, unlike sustainSeconds, it contains in it a bit of hints that suggest why we need it.

The tempo fix feels like the most conservative and straightforward (I’ve also been using that specific change for years, so it’s ostensibly well-tested :slight_smile: )

Fair enough, though I expect that if someone were using sustainSeconds or sustainAbs as a control name with the event system creating the synth, then their explicit setting in the event should override any new internal logic, so behaviour should not change? (I’d guess a so-named control would have the same intention/behaviour anyway…) So IIUC, it would only be a risk if there was a control with that name that wasn’t previously set by the event?

That seems fairly low risk, unless I’ve misunderstood, so personally I’d say it’s worth going in to save the user the explicit conversion in a synth. It feels like something that should happen in the event.

Agreed. Good catch!