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?
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:
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.
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.
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?
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 }
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.
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?
I wonder, what are the cases where this would change behavior?
Event does not specify a \tempo?
old: clock tempo is unchanged
new: clock tempo is unchanged
Event specifies explicit tempo
old: clock tempo is set to ~tempo
new: clock tempo is set to ~tempo
~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
~tempo is referenced elsewhere in Event, and it IS defined
old: ~tempo value is used
new: clock.tempo.
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.
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.