Patterns with fixed dur Envelopes

Play-function approach A (also see note below):

diff --git a/SCClassLibrary/Common/Collections/Event.sc b/SCClassLibrary/Common/Collections/Event.sc
index 9ceda30b6..77f68d9b6 100644
--- a/SCClassLibrary/Common/Collections/Event.sc
+++ b/SCClassLibrary/Common/Collections/Event.sc
@@ -243,7 +243,7 @@ Event : Environment {
                        ),
 
                        durEvent: (
-                               tempo: nil,
+                               tempo: { thisThread.clock.tempo },
                                dur: 1.0,
                                stretch: 1.0,
                                legato: 0.8,
@@ -449,7 +449,11 @@ Event : Environment {
                                        ~finish.value(currentEnvironment);
 
                                        tempo = ~tempo;
-                                       tempo !? { thisThread.clock.tempo = tempo };
+                                       tempo !? {
+                                               if(tempo.isFunction.not) {
+                                                       thisThread.clock.tempo = tempo
+                                               }
+                                       };
 
 
                                        if(currentEnvironment.isRest.not) {

Play-function approach B:

diff --git a/SCClassLibrary/Common/Collections/Event.sc b/SCClassLibrary/Common/Collections/Event.sc
index 9ceda30b6..8dcdc8c99 100644
--- a/SCClassLibrary/Common/Collections/Event.sc
+++ b/SCClassLibrary/Common/Collections/Event.sc
@@ -449,7 +449,11 @@ Event : Environment {
                                        ~finish.value(currentEnvironment);
 
                                        tempo = ~tempo;
-                                       tempo !? { thisThread.clock.tempo = tempo };
+                                       if(tempo.isNil) {
+                                               ~tempo = thisThread.clock.tempo;
+                                       } {
+                                               thisThread.clock.tempo = tempo
+                                       };
 
 
                                        if(currentEnvironment.isRest.not) {

msgFunc approach:

diff --git a/SCClassLibrary/Common/Audio/SynthDesc.sc b/SCClassLibrary/Common/Audio/SynthDesc.sc
index 504c547a2..5ffde9747 100644
--- a/SCClassLibrary/Common/Audio/SynthDesc.sc
+++ b/SCClassLibrary/Common/Audio/SynthDesc.sc
@@ -442,6 +442,9 @@ Use of this synth in Patterns will not detect argument names automatically becau
                                if (name != "?") {
                                        if (msgFuncKeepGate or: { name != "gate" }) {
                                                if (name[1] == $_) { name2 = name.drop(2) } { name2 = name };
+                                               if(name == "tempo") {
+                                                       stream << "\ttempo ?? { tempo = thisThread.clock.tempo };\n";
+                                               };
                                                stream << "\t" << name2 << " !? { x" << suffix
                                                        << ".add('" << name << "').add(" << name2 << ") };\n";
                                                names = names + 1;

All tested with this code:

(
SynthDef("sinestempo",
	{ arg out=0, freq=440, sustain=0.05, amp=0.1, pan, tempo;
		var env;
		env = EnvGen.kr(Env.sine(sustain / tempo), doneAction:2) * amp;
		tempo.poll(Impulse.kr(0));
		Out.ar(out, Splay.ar(SinOsc.ar({freq * Rand(0.98, 1.02) } ! 20, 0, env), spread: 1 - pan, center: pan))
	}
).add;
)

p = Pbind(\instrument, \sinestempo).play;

TempoClock.tempo = rrand(1.0, 2.0);

p.stop;

For the play-function “\tempo as a function” approach A, one question is: should tempo: { something } in an event set the clock’s tempo? This diff says no. First, nobody is doing that currently because the default event prototype barfs on any non-nil tempo value that is not a simple number – it’s not breaking a currently-valid usage.

Second, currently, if an event is play-ed, and one of its keys is needed for an arg list, and that key is populated as a function, then the function is resolved at the last second, just before sending. Up until that moment, it’s passed around only as a function. So I’ve retained that – a \tempo function remains an abstract value except for final resolution in the arg list. (If a user wants to calculate a tempo for every event, using a function, and make it a concrete value, they can write \tempo, Pfunc { ... something ... } – which is exactly what they have to do now, so, no breakage. I think users should not be encouraged to exploit event-keys-as-functions except for rare cases where there’s no other way.)

If it would be desirable to evaluate a tempo function upfront, then at least do a check so that it calls tempo_ only when the tempo is actually different. I don’t think it will make a difference for TempoClock, but I’m not sure we want LinkClock, Scott W’s BeaconClock, or my DDWLeadClock and DDWFollowClock to broadcast repeated non-changing tempo changes. (Not calling tempo_ at all for a tempo function naturally avoids this.)

hjh

This feels extremely non-standard. Functions as event values is a very common pattern, and in almost all cases these are unwrapped with a .value call before consuming. Having ONE specific key that behaves differently than most others w/r/t unwrapping just creates potential for confusion?

Likewise, in the second if(tempo.isNil) {} case - this effectively just implements a default value for this key, but in a different way than every other event value. This way of expression this doesn’t seem to gain anything, but definitely creates a whole bunch of edge cases where this one specific key doesn’t behave like any other key.

If a user wants to calculate a tempo for every event, using a function, and make it a concrete value, they can write \tempo, Pfunc { ... something ... } – which is exactly what they have to do now, so, no breakage. I think users should not be encouraged to exploit event-keys-as-functions except for rare cases where there’s no other way.)

Hmm, I think I disagree here? Functions as event values is a critically important feature - it’s the only way to evaluate an Event value in the context of the fully constructed Event (since Pfunc only has access to - at most - the partially constructed event in the context of the specific Pbind it’s a part of).

I really think a change like:

tempo: { thisThread.clock.tempo }
tempo = ~tempo.value;
if (thisThread.clock.tempo != tempo) { thisThread.clock.tempo = tempo }

…behaves exactly the same as the current implementation in all common cases, and makes a bunch of slightly less common cases much more intuitive. It removes some of the special-case-ness from the \tempo key, and simplifies the implementation.

If this were a classlib change, I would be inclined to skip the equality check here and instead just guarantee in the various clock implementations that tempo_ calls that do not change the tempo should be no-ops. This makes the Event implementation even more simple, and usually this kind of “no change” optimization should be part of the callee and not required by the caller.

But this is my point: The Patterns guide says sustain is ‘How many beats to hold this note’. This is the current behaviour. If it converted to seconds it’d solve my problem, which is how to scale fixed envelope durations so you don’t get different ‘legato’ behaviour at different tempi. Just to be clear:

SynthDef("sinessust",
	{ arg out=0, freq=440, sustain=0.05, amp=0.1, pan, tempo;
		var env;
		env = EnvGen.kr(Env.sine(sustain.poll), doneAction:2) * amp;
		Out.ar(out, SinOsc.ar(freq, mul: env ));
}).add;


Pbind(\instrument, \sinessust, \dur, 1, \legato, 0.5).play

TempoClock.tempo = 1; // UGen(OutputProxy): 0.5
TempoClock.tempo = 2; // UGen(OutputProxy): 0.5 but should be 0.25

So really what I’m looking for is absolute duration in seconds to be passed to the synth if some key is present in the synth. The tempo idea is really just one way of working around that, though it may well be useful for other reasons.

A good point, but I think that protection could reasonably be in the clock implementation? BeaconClock is a little funny with that as it tries to roughly model multiple agents, rather than a networked ‘metronome’, so it’s sending tempi all the time anyway.

@muellmusik

The Patterns guide says sustain is ‘How many beats to hold this note

I think that is a typo in the documentation. The sustain key is always an absolute value in seconds AFAIK .

My point was that a function for tempo does not currently work. You will get an error – but only for this key.

I don’t necessarily object to a tempo value being provided by a function then changing the tempo, actually. But it isn’t a currently supported usage – go ahead, try it, you’ll see. I’m only saying that my suggestion wouldn’t break anything that is currently working. We could certainly change it to make it work, sure.

hjh

See my example. It’s not, or?

If that’s a bug though, fixing it would solve my problem.

I am not sure if it is a bug or not. I mostly handle it from the client side like this:

(
TempoClock.default.tempo = 1;
Pbind( \instrument, \sines, \dur, 0.1, \sustain, Pfunc{|ev|ev.dur / TempoClock.default.tempo }).play
)

(
TempoClock.default.tempo = 2.5;
Pbind( \instrument, \sines, \dur, 0.25, \sustain, Pfunc{|ev|ev.dur / TempoClock.default.tempo }).play
)

Re-reading this: I agree with this suggestion, with one addition: to assign the result of ~tempo.value back into the Event, because the user will probably assume that a \tempo function would evaluate only once. But, if the play function evaluates it in order to set the tempo, and the function gets passed through unchanged into the arg list, then the arg list will evaluate it a second time.

There is also ~finish, fwiw.

The event-keys-as-functions technique relies on invisible magic, so, all other things being equal, I would usually prefer a more explicit solution if one is available. BUT… once upon a time, I did have a case where even ~finish wouldn’t have helped: an event type that allocated buses to use for a sub-event contained within the main event. The buses were allocated in the event type function, which happens after ~finish, so the only way to get the Bus object(s) was to reference them in arg-list functions.

hjh

… so just for clarifying this: we would expect sustain to be in seconds (independent of tempo) and legato to adjust to tempo?

I think everywhere I can find sustain discussed it is defined in beats. So I assume that is correct. What we need is a way to get the corresponding value in seconds and/or pass the tempo to the synth so it can scale sustain correctly.

I think we’re thinking of this from different directions, hence the confusion. All Event values that end up being sent to a Synth are eventually .value unwrapped because of asControlInput. Event values that are consumed by something other than a synth are maybe .value unwrapped, or maybe not (~sustain and ~synthDefName are, for example, ~tempo is not). (I know you know this stuff, just re-iterating for clarity and for anyone reading along who doesn’t).

Ultimately, the user perception here is simply that: functions as event values mostly work intuitively, and sometimes fail to work with a probably very obscure error far away from their code (since many generic operations tend to be valid on functions, these only explode later on when a real value is needed). This is bad, and is a huge cause of unpredictable behavior with the Event system (which is mostly related “special case” behaviors for specific keys). With this in mind: you’re thinking of the ~tempo case as one where functions have never worked (because they’re not supposed to) - I’ve long thought of the ~tempo case as a bug, a place where the value should be unwrapped like most other values, but isn’t.

There is a very valid and essential use-case for functions as Event values: to be able to calculate a value in the context of a completed Event. There’s no straightforward replacement for this behavior, so a “don’t use functions as event values” path isn’t really feasible - we need a way to provide this behavior.

Probably the truly correct solution here is something like a phase after (or maybe before?) ~finish where every Function-like value is unwrapped like a Thunk in the event - for example:

currentEnvironment.keysValuesDo {
  |key, value|
  if (value.isKindOf(AbstractFunction)) {
    currentEnvironment[key] = value.value();
  }
}

This is probably more performant in common cases than the current approach? It likely fixes some edge case bugs, but also breaks some other use-cases that don’t have straightforward replacements (specifically, I’m thinking of using Routines as event values to do lazy multi-voice expansion - it’s very hard/impossible to reproduce this any other way). In the end it’s a bit academic: this is not at all backwards compatible, so it’s probably something more like new + refactored event system.

In the mean time, my personal feeling is that more consistent unwrapping values is better than sticking with current pattern of “it’s unwrapped if it’s a synth arg or it’s a key that the event system happens to unwrap” approach - unwrapping will only make MORE cases work correctly (e.g. tempo), and has very few downsides I can think of (unless there is user code somehow relying on functions NOT being unwrapped for specific keys, which feels… very weird?).

Actually, I don’t think that – “because they’re not supposed to” is your inference, not at all my intent.

My original suggested code was just that: a suggestion – a point of discussion to be amended as needed.

It seems to have been interpreted as “a position,” to which your view exists in opposition. I think rather that we’re both trying to figure out what to do, and that any “positions” are provisional, and that we could work cooperatively toward the best answer. (I don’t have all the answers, and neither do you, but working together, we can make progress.)

That means, from where I sit, this thread has taken on a little bit of a combative edge – i.e., I open the thread and find pages of response to a point from post #26, where later in post #29, I’d written “Re-reading this: I agree with this suggestion [yours]…” I mean… I was really trying there to back this away from an argument… which effort seems to have failed.

Actually I think evaluating these functions prior to calling the event type function is not a bad idea. It would break my bus-allocator event type, but in ddwChucklib, I could just define that as a different event type altogether, with different play function logic.

hjh

Sorry to come across as argumentative, that wasn’t my intent but I can see how it reads that way! Finding a way to clean up some of this Event unwrapping stuff has been a long-time effort for me - mainly just trying to tease out the implications of different approaches, and critique the current state of affairs a little. The argument isn’t with you, only between approaches that I’ve been struggling with myself. :slight_smile:

1 Like

I appreciate that… I also might have served the discussion better by thinking it through before posting, rather than disagreeing initially and then walking it back, since this just created opportunities for misunderstanding.

My initial “position” (while I still hesitate to call it that) was “minimal change” to the interface – to make it work to send tempo in OSC without altering much else, which is conservative by default. A conservative approach to code changes is not always the right approach. I suspect the closer to the core one gets, the more appropriate conservatism becomes – but Events aren’t in that category.

The whole function-in-event thing is quite messy, and I’m not convinced it was ever properly designed, but rather was a happy accident from asControlInput. I’m definitely open to rethinking it.

But, also a question for this thread: is \tempo, { thisThread.clock.tempo } the right solution to the stated problem? Maybe it isn’t.

hjh

1 Like

Would it be reasonable to say that it is good practice to postpone evaluations and conversions to the latest possible point?

I understand that sometimes, we want it early, because it makes bugs easier to find (e.g. type-test like things), or because we need a value of a particular kind (e.g. the evaluated current tempo in our case here).

Calling all functions in an Event to finish it assumes that we never return functions from events. I think it is better to pass them on and let them evaluate further down.

But: if we had a simpler way to express which functions are to be evaluated, that would make things easier, perhaps.

For example a method like:

callKeys { |keys|
    this.use { keys.do { |k| k.envirPut(k.envirGet.value)) } }
}

This method can then be called explicitly in event types.

Interesting! I didn’t know that. From the perspective of the SynthDef it was the absolute time, which is why I never looked at it from the other side.

When you wrote:

This indeed seemed wrong, but had you written:

Pbind(\instrument, \sinessust, \dur, 1, \sustain, 0.5).play

I would have expected it to be 0.5 at all tempi. I do see your point. But at least it is important to be able to specify this independently of tempo (e.g. for some cases of granular synthesis, you may want the grain length to be independent of tempo).

So when scztt writes:

It seems to me that we need another level of duration (in analogy to the levels note - midinote - freq), which is relative to tempo.

Then for each timingOffset and sustain we have three levels:

  • dependent on dur and tempo (<nonexistent> and legato)
  • dependent on tempo (timingOffset and <nonexistent>)
  • the value in seconds (lag and sustain)

Generally, sepaking, for all values of an event we have a way to override all smart calculations, when we specify the control value of the SynthDef, like freq and not some other, more indirect value, like note.

Yes, this makes sense, and adds some helpful nuance. I think sustain should say in beats though, as to change it would be odd at this stage. We need a sustainAbs or similar. Might be nice to adopt that nomenclature from the Clock interface.

Hm, in my experience, despite the fact that the helpfile says something else, sustain has always been independent from tempo, when set explicitly:


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;

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

I see that the topic of this thread is that this should change, but to me it is not entirely clear if maybe the documentation is wrong. At least the synth control is usually the most basic / direct value, not modified implicitly …

I certainly see your point. I’d interpret it slightly differently though. When the synthdef has a control that matches the name of a key, it’s passed as as it is, whether explicitly defined or not. I think the semantics here are slightly confused by the fact that if tempo = 1, which is a very common case, then sustain in beats or seconds is the same. Certainly that’s why I never noticed.

Still, it’s possible changing the semantics to always be in seconds could break something for someone. And the documentation does say it’s in beats.