I want a synth arg to represent an envelope, and I want IEnvGen to read that envelope at arbitrary indexes. I can make it work, but I feel like there’s one step to making it better. Here’s the deal:
If I do this (on Windows, SC 3.13), the server crashes:
So, how can one write the initial def so that you don’t have to set the argument on initialization (like, if you were going to use a NodeProxy instead of a SynthDef)?
And, actually, it doesn’t really work in the second instance, because if you set the \env to something else while the synth is playing, you get no change from IEnvGen.
envelope: an instance of Env (this is static for the life of the UGen)
It can’t be modulated after synth creation (which also means that it’s mandatory to set upon synth initialization).
In a NodeProxy, I think you can set parameter values before assigning the signal source. Once set, the parameter exists in the proxy’s nodeMap and (I think, going from memory here) that the nodeMap’s values will be sent when initing the synth.
Edit: Also, you can put a default Env in here – \env.kr(Env([0, 1], 1, 4).asArrayForInterpolation.unbubble.extend(n * 4, 0)) where n is the maximum number of segments you expect to need. (You don’t have to use all of them but you do need to pre-allocate.)
I haven’t actually tried this with IEnvGen, so some details might need to be tweaked. The general technique for envelopes is fine.
Oh, so I’m supposed to be reading the help files now?
Thanks for the info. It seems that the only way to read a variable envelope from an arbitrary index on the server without launching new synths is to use a buffer, then? Might I be missing something? The buffer method is fine with me, it just seems like there could be another way.
EnvGen reads the next control point (at kr) when a segment ends – and crucially, this is a specific point in time: the envelope segment that is currently evaluating began at a specific moment, and it will deterministically end at that moment + segment duration. So EnvGen doesn’t need to poll the entire envelope array continuously (a potentially large CPU drain); it can read endpoint values for the next segment only at the moment when the new data are needed.
In IEnvGen, if you give it Env([0, 1, 0], [0.5, 0.5]) and the input time index is 0.4, then the segment index = 0, and you know that it will cross over to segment index 1 when time index >= 0.5. Except… if the entire envelope is modulatable, then you could replace the envelope with, say, Env([0, 1, 0], [0.1, 0.9]). It would be wrong to continue with segment index = 0: time = 0.4 was 80% of the way through segment 0, but is now 33% of the way through segment 1. Now, how does IEnvGen know that? There’s no trigger to let it know that the envelope changed, so AFAICS, the only reliable way would be if IEnvGen re-evaluated the “integrate” operation on all of the segment times for every output sample (or at minimum, every control period). If you anticipated a large number of control points, say \env.kr(Env.newClear(100).asArray), now every sample is iterating over a hundred time values → user questions like “I switched this SynthDef over to use IEnvGen and now my CPU use is blowing up.”
One way would be to deprecate IEnvGen and replace it with a new unit where there is a trigger to tell it that the envelope changed. (I don’t think it would work to re-evaluate the envelope when the time index changes, because 1/ above points out that it’s valid to change the envelope without changing the time index, and 2/ the normal use case is a continuously changing time index with a (currently) fixed envelope, which would not avoid the heavier CPU cost even when the envelope didn’t change.)
One last detail – I think .collect(_.reference) is not needed here. unbubble seems to be needed but there’s no need to .reference. (Refs to arrays are needed for Klank, Klang etc but not for EnvGen or IEnvGen.)
Thanks for the message
I could for example imagine that IEnvGen derives a trigger from its ramp input and if there is a request to change the data from the language, then the new data is temporarily stored in some internal memory and released on the next ramp cycle. Currently preparing envelope shapes in the language, storing them into buffers and playing them back as tables is more flexible then IEnvGen, because you could at least change the current buffer if you want.
I think there is some implicit thinking about data from the language perspective involved with this Ugen or others. Instead of passing for example 8 different durations, one could also just use 2 and modulate the different params over time. On the server it makes more sense to me, to have modulatable params for example like Env([0, 1, 0], [skew, 1 - skew]) instead of passing large sets of fixed durations.
That’s an interesting idea. I’m not sure which way is better.
For fun, I compared it to the [function~] object from the Else library in Pd. It’s basically the same idea as IEnvGen – n+1 levels, n times, but only linear segments, with a signal rate input. When the envelope changes, it just jumps to the new output level – which is a fine behavior. I don’t think it’s necessary to wait until the time input crosses a segment boundary to update the endpoints.
[function~] avoids the problem of scanning the inputs for changes because it can accept both signal input and control messages directly. So [function~] can just calculate input and wait for a new list, updating only upon receipt of that list. SC UGens can accept only signals (ar or kr – kr is still a signal!). So “temporarily stored in some internal memory” would be the only way, given the current input format, to detect a difference between the current input values and the previous ones (which is separate from the question of when to activate the new values).
At this time, I’d probably lean toward a solution where the envelope data are stored in a RTAlloc-ed array, and the output is calculated based on that array (not directly on the control inputs). Something would need to look for changes and update the internal array accordingly. But I’m not wedded to that – if someone’s got a better idea, go for it.
I think I see your point, though sequencing segments continuously could be complicated this way.