Patterns with fixed dur Envelopes

One of those slightly funny things I’ve never quite noticed before, relating to fixed duration envs in patterns…

I would expect these two examples to sound the same, since the duration model should account for sustain in beats.

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

TempoClock.default.tempo = 1;
Pbind(\instrument, \sines, \dur, 0.1).play


TempoClock.default.tempo = 2.5;
Pbind( \instrument, \sines, \dur, 0.25).play

However, the default event is not clever enough to detect scale the sustain value based on tempo before sending to the synths given the synthdef has no gate argument. So the second stream’s ‘notes’ are too long and overlap.

Is this by design, an oversight, or a misunderstanding on my part?

Hmm. It’s the same issue with gated envelopes. I can make it work with something like this:

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

TempoClock.default.tempo = 1;
Pbind(\instrument, \sinestempo, \dur, 0.1, \sustscale, Pfunc({ thisThread.clock.tempo.reciprocal })).play


TempoClock.default.tempo = 2.5;
Pbind(\instrument, \sinestempo, \dur, 0.25, \sustscale, Pfunc({ thisThread.clock.tempo.reciprocal }) ).play

But surely that’s not the design intention? I would expect there to be some intelligent convention for communicating the sustain time in seconds to the synth?

I’ve hit this ambiguity before also. It seems like in general, the principle is that synth parameters are generally interpreted to be “raw” time values in seconds - this probably makes sense, as (1) it would be hard or impossible to figure out which synth parameters are supposed to be time values and scale them all automatically, (2) the workaround is relatively easy (just scaling by tempo yourself), and (3) it’s easy to get super unexpected results (stretching ALL envelope parameters would be strange if you weren’t expected it, but sustain is often used for scaling Envelopes).

I often build synths with a tempo parameter and do the multiplication in my synth if I want tempo-locked envelopes / oscillators etc.

It might feel more correct if you removed tempo from ~sustain before you sent it as a synth parameter, but I still feel like there would be some… unclear expectations in this case too? Something like:

sustainBeats = ~sustain.value;
~sustain = sustainBeats / ~tempo.value;
1 Like

It feels to me like something the note event type should be aware of and be a bit ‘smart’ about, like detecting a gate control or converting pitch models. At this stage I’d suggest detecting a tempo control in the synthdef and if found just sending that along with each event. It’s not how I’d do it from the scratch but I don’t think that would cause any problems? Should allow the user to make use of that info in whatever way is necessary/appropriate.

Unless I’m misunderstanding you, as long as you have a tempo control in your synth, it will automatically pull tempo from the event, so this doesn’t take any extra plumbing at all.

Perhaps I’ve misunderstood, but I think some plumbing is needed as tempo doesn’t come through automatically.

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

Pbind(\instrument, \sinestempo).play

     1047 sinestempo
        out: 0 freq: 261.62557983398 sustain: 0.80000001192093 amp: 0.10000000149012 pan: 0 tempo: 0

This forwards, but won’t track changes…

Pbind(\instrument, \sinestempo, \tempo, TempoClock.tempo).play

Woah weird - for me, this works fine, even with no explicit tempo:

(
SynthDef(\test, {
    Out.ar(0, Env.perc.kr(1, doneAction:2) * SinOsc.ar(\tempo.kr().poll * 100))
}).add;

Pbind(
    \instrument, \test,
    \tempo, 2.2
).play
)

An unfortunate complication is that tempo already has a meaning in the default event prototype: to change the current tempo, if non-nil.

					tempo = ~tempo;
					tempo !? { thisThread.clock.tempo = tempo };

Perhaps use bps, then? (Or spell out ‘beatsPerSec’, probably better.)

If we add a pair bps: { thisThread.clock.tempo } into durEvent, then, if the synthdef has a bps control input, this function will be evaluated in asOSCArgArray and the value would go through to the synth. Then there would be no need to update multiple event types.

Then:

(
SynthDef(\test, { |out, gate = 1, freq = 440, bps = 1|
	var eg = EnvGen.kr(Env.asr(0.01, 1, 0.01), gate, doneAction: 2);
	bps.poll(Impulse.kr(0));
	Out.ar(out, (SinOsc.ar(freq, 0, 0.1) * eg).dup);
}).add;
)

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

UGen(OutputProxy): 1
UGen(OutputProxy): 1
UGen(OutputProxy): 1

TempoClock.tempo = 1.8;

-> TempoClock
UGen(OutputProxy): 1.8
UGen(OutputProxy): 1.8

p.stop;

hjh

Yes, if you specify it in the Pbind it will come through. And as @jamshark70 points out you can have tempo as a key, and change it with each event.

Would be soooo much nicer though if it just sent whatever the tempo is providing the synth has a tempo control, whether it’s explicit or not.

James, yes I know there’s already a tempo key, so I don’t really see the need to add another key which just duplicates it?

All we need is to check if the synth has a \tempo control and if present, forward the current tempo whether it’s been explicitly set in the event or not. I don’t think there’s any issue with doing that as it’s just an additional side effect and wouldn’t need to change behaviour at all?

Ah just to be clear, my example works without an explicit tempo key. But now that I think about it, I may have fixed this myself somewhere, because I do recall some related problems a long time ago. I can’t for the life of me see why this would NOT work in normal cases, nor where I might have fixed it. AFAIK the default implementation of getBundleArgs should currectly pull ~tempo from the environment if it’s a SynthDef argument?

				getBundleArgs: { |instrument|
					~getMsgFunc.valueEnvir(instrument).valueEnvir;
				}.flop,

This should work?

(
~event = (
    func: {
        |tempo|
        "tempo is: %".format(tempo.value).postln
    }
).parent_(Event.default);

~event.use {
    ~func.valueEnvir();
}
)  

For me that posts tempo is: nil.

I tested the control polling approach on an SC with no extensions just to be sure, and also nil.

Ah, I found the place where I fixed this:

	Event.parentEvents.do(_.put(\tempo, { ~bpm !? { ~bpm.value / 120.0 } ?? { 1 } }));

Normally, ~tempo is explicitly nil in the default Event. This feels like a typo, since tempo: nil is meaningless when defining an event. I think it’s supposed to facilitate this:

tempo !? { thisThread.clock.tempo = tempo }

But, this could also be expressed as this to fix the problem:

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

That looks about right to me! Should we knock up a PR?

The current tempo key is a “write” key – it changes the current tempo on the thread’s clock.

What you’re asking for is a “read” key – get the current tempo.

Changing the current “write” semantic into a “read” semantic would be a breaking change and should not be done casually. Hm, but if the play function were changed so that it sets the tempo only to a number (not a function), then I think it could be done in one place, instead of in every event type.

hjh

Sorry James, I’m not sure how it would break anything. If the tempo has been set by the event, we send the tempo. If the tempo has not been set, query the thread’s clock and send that. No behaviour need change, or? Can you explain a little more?

S.

I wonder, what are the cases where this would change behavior?

  1. Event does not specify a \tempo?
    old: clock tempo is unchanged
    new: clock tempo is unchanged
  2. Event specifies explicit tempo
    old: clock tempo is set to ~tempo
    new: clock tempo is set to ~tempo
  3. ~tempo is referenced elsewhere in Event, and it IS NOT defined
    old: nil - probably this is an error, unless there’s code like ~tempo ?? {some other value}
    new: clock.tempo
  4. ~tempo is referenced elsewhere in Event, and it IS defined
    old: ~tempo value is used
    new: clock.tempo.
  5. Synth has \tempo control
    old: only sent if explicitly specified
    new: sent even if not explicitly specified

That seems to be in response to the part of my post which I struck out… because I thought about it a bit more and realized that I didn’t really have an objection. strike through = recant.

I can think of three ways to implement it:

  • Edit every new-synth event type to append a tempo pair to the arg list if the control exists. Repeated work, not highly in favor of this.
  • Edit the default event’s play function so that it sets tempo on the clock only if the event provides a SimpleNumber for tempo. Then the durEvent can have tempo: { thisThread.clock.tempo } and this is resolved by asOSCArgArray. Pro: Behavior is localized to the default event. Con: Depends on asOSCArgArray magic.
    • Or, maybe even better, the play func could resolve tempo before calling the event type func.
  • Edit SynthDesc so that the msgFunc looks up clock tempo if no tempo is found in the event. Pro: the arg array would only ever have a number for tempo (no magic). Con: Affects every consumer of msgFunc, whether they want it or not.

I think I lean toward the second option, but the third would be ok with me too.

hjh

What about
Pbind(\instrument, \sinestempo, \tempo, { TempoClock.tempo }).play
?

This one should:
Pbind(\instrument, \sinestempo, \tempo, Pfunc { TempoClock.tempo }).play

In general, there are three different ways of thinking about what “sustain” means:

  • absolute, ins secs (this is implemented as sustain)
  • relative to dur, in secs (this is implemented as legato)
  • relative to a tempo, in beats (this is not implemented)

A similar (but not exactly similar) case is the delay:

  • absolute, in secs (this is implemented as lag)
  • relative to dur or sustain, in secs (not implemented)
  • relative to tempo, in beats (this is implemented as timingOffset)

This is basically the same as my durEvent suggestion (if the play function is amended to avoid trying to set the tempo to a non-numeric object) – except if it’s in durEvent, then the user doesn’t have to write it, and it uses the thread’s clock instead of hardcoding the clock.

hjh