Why not make presamples an argument?

Hi all,

If this comes across as negative or a rant, that is not the intention. I’m also not a programmer so bear that in mind.

It seems that a ‘presample’ is a concept used only when a UGen is an input to another UGen that requires one (or more than one). There have been lots of debates about how to do presamples correctly. Why don’t UGens that require presamples just have one or more extra arguments to define the presamples?

For example, K2A.ar(signal) needs to read signal’s presample so that it has something to start interpolating from. If K2A.ar always interpolated from 0, regardless of the input signal, this would cause problems in a minority of cases. But something like K2A.ar(signal, presample: 0.5) would surely solve that, with the ‘presample’ argument presumably defaulting to 0.

Possible reasons I’ve thought of:

  • Things have always been this way
  • Not enough demand for individual sample control in signals
  • Would break compatibility with current implementations
  • The complexity in writing/maintaining UGens would increase too much

Any perspective appreciated (:

It’s part of the UGen “contract” that its constructor (the function that initializes the UGen) should set its output to one initial sample. All UGens are supposed to do this. Then, this value is available to any downstream units.

I think the best reason not to allow users to override the pre-sample is that it’s just one more thing for users to mess up.

In fact, K2A does not interpolate from 0.

// starts with 0
{ K2A.ar(Impulse.kr(ControlRate.ir * 0.5)) }.plot;

// starts with 1
{ K2A.ar(Duty.kr(ControlDur.ir, 0, Dseq([1, 0], inf))) }.plot;

This is one of those cases that has been debated. The problem is that the definition of “pre-sample” was never really solidified. Is it the first output sample (the Duty example), or the value that precedes the first output (the Impulse example)? I’ve argued in the past that we should stabilize this, so that it’s always the first output sample. But that’s not risk-free (particularly here because Impulse is usually used as a trigger – FWIW I don’t fully agree with this counterargument, but my point of view didn’t carry that day), and it was decided at that time not to change the few cases that deviate.

Do you have a specific case that isn’t working for you, that’s motivating this question?

hjh

Yeah the value that precedes the first sample doesn’t make sense in a lot of cases.

I strongly agree with you James that the lack of a standard, and the inconsistency, are a big problem. Quite a few ugens (particularly Impulse) simply don’t work the way that anyone would expect. Other ugens don’t work as expected when you combine them (because they use different definitions of pre-sample).

I actually never understood why this was necessary. Every explanation I’ve seen has never made sense to me.

To state it more formally, let’s call the first output sample y(0). A UGen’s Ctor function needs to initialize the output, before the “real” output begins. Most UGens assume that this pre-initialization should use y(0), but there’s a small subset of units such as Impulse that initialize to y(-1).

I’m not sure that it’s “quite a few” – I think the number of affected units is small. But Impulse is critical!

Anyway, when this came up before, I argued: UGens init to unexpected values · Issue #2343 · supercollider/supercollider · GitHub

Or, simpler, mandate that all UGens must initialize to the first true output sample. Then, trigger-receiving units may ignore the value from the input’s Ctor and initialize to 0 instead.

There was discussion about whether the Ctor should initialize to y(0) or y(-1). I’m kind of thinking:

  • If it’s y(0), it’s fairly easy for units to ignore the incoming value if they need to (e.g. to respond to a trigger when calculating the first real output sample).
  • If it’s y(-1), then some units will have to wait until the first calculation block to get the initial value. That’s more complicated and easier to break.

But, where we pretty much left it was: UGens init to unexpected values · Issue #2343 · supercollider/supercollider · GitHub

Fixing this issue will take a lot of work and a lot of API changes. Furthermore, there are still plenty of absent and inconsistent initialization issues in sc3-plugins and other third-party plugins.

I’m still not sure what is “a lot,” and the trouble it causes is also IMO “a lot.” At the same time, it’s also true that sclang/scsynth have other gotchas that are worse than this.

And, it’s currently possible to work around the Impulse problem using demand units.

hjh

At least in the case of Impulse, I believe https://github.com/supercollider/supercollider/pull/4150 fixes the presample issue, along with a bunch of other impulse bugs and problems.

Hi all, sorry for starting this then disappearing. I haven’t used SuperCollider for a while so don’t have any legitimate examples ready to go.

But a few things I remember:

  • Using Delay1.kr, I think the Delay1 UGen would not have the right presample for some purposes.
  • Using RecordBuf.kr and PlayBuf.kr to make a big complicated feedback thing — I’d often have to set the presample (and occasionally even the first sample for various reasons), by adding the result to another 2 UGens whose sole purpose is to output a single value at the the presample/first sample, which is hard work.
  • I can’t really remember how the triggering stuff works, but sometimes I would want the first impulse to trigger something, and other times I wouldn’t, and I remember the presample having some influence here.

Anyways, just a crazy thought, would it be plausible for someone with not much development experience to branch the most important presample-dependent UGens into a plugin, so that a new UGen like PSK2A.ar would take a presample argument? I’m guessing that would seem like a crazy solution for many reasons, but it would have the solid advantage of not breaking anything while providing a solution to any obscure presample accuracy case…

Thanks James.

I guess I’ve never really understood why a UGen should set it’s output out to the initial sample during Ctor.
But that’s more of a question about the graph engine in SuperCollider.

It would be possible to make the Ctor just a stub to set the rate-dependent calculation function, and push all initialization into the _next functions. But… if you decide “init in next,” then you have two choices:

  • The _next function begins with if(!initialized) { ... init... }. This incurs a branch ControlRate times per second, but the branch is executed only once. Recurring cost for a one-time action is not efficient. (We should be able to agree that efficiency is highly important in the server.)
  • Or, you could have _next_with_init and _next – Ctor sets the function to the with_init version, and with_init sets the function to the normal _next. This would double the number of functions – and some UGens have 2, 4 or 8 next functions to optimize the handling of different input rates. Say goodbye to code maintenance, then.

IMO neither of those options is particularly appealing.

FFT is one case where the initial output is necessary.

var fft = FFT(LocalBuf(2048, 1), someSignal);
fft = PV_RandComb(fft, 0.5);

During init, PV_RandComb needs to know about the fft buffer (to allocate its own storage), but it gets only the output of FFT. So FFT must supply a Ctor sample, or rearchitect the server and all plugins following one of the options above. Ctor is better for efficiency but, as noted, requires care in the implementation that has not always been taken.

hjh

Hm. We should wake that PR back up. Seems it’s been forgotten.

hjh

Yes, there are definitely cases like that. I think these are the fault of the upstream unit, though. I still think the best solution is to identify the plug-ins that output the wrong initial sample.

It shouldn’t.

Internally, triggers work like this: UGens with a trigger input have a member variable prevtrig. A trigger happens when prevtrig <= 0 && trig > 0. At the end of every cycle, prevtrig is set to trig (basically a one-sample delay).

I grepped the plug-ins source code when I was researching this a couple of years ago, and found that unit->mPrevTrig is always initialized to 0 (except for one stupid case in one of the Convolution units, where one line does unit->mPrevTrig = 0; and the very next line does unit->mPrevTrig = ZIN(0); – which is completely ridiculous – must have been done before we improved code review practices).

I think “init prevtrig to 0” is a good policy – simple, easy to remember, and easy to explain to users.

The consequence is that, if an incoming trigger input starts with 1, then this will always be a trigger. That’s also a simple rule, easy to remember, and easy to explain to users. (And, if you want no trigger at the beginning of the synth, make sure the first input is 0.)

Maybe there is something in sc3-plugins that initializes prevtrig incorrectly, but that would violate a pattern that is never broken in the core plugins.

That’s not a crazy idea – it was proposed in UGens init to unexpected values · Issue #2343 · supercollider/supercollider · GitHub. But I’m not really good at plugins so I never finished it.

hjh

Did you have a look at Fb1 from miSCellaneous_lib quark? You can set arbitrary presamples (see inInit and outInit args). You could also use it to define pseudo ugens with your preferred behaviour.

I’ve looked into this in great detail already and documented it HERE, including summarizing where the discussion was left.

Here’s what I proposed for UGen initialization going forward:
(I had to scrub most links out because of a link limit, see original post if you’re interested)

I’ll summarize what I’ve gathered from [#2333], [#2343] and propose these guidelines for initializing UGens:

  • The initialization sample and the first sample should be equal and both represent the unit’s
    output at time 0, y(0). Make no assumptions about how your signal will be used downstream (e.g. as a trigger).
  • If the initialization sample represents anything other than y(0), it needs to be clearly documented as and exception to the rule, what it is and why.
  • For units requiring values for y(-1), y(-2), etc, it’s the author’s responsibility to
    • a) provide an interface to them. I’d consider this best practice: optional arguments to the UGen to directly provide these values.
      • See the zi argument in scipy.signal.sosfilt, and
      • the skip argument in CSound’s tone.
    • or b) set these manually with values that are sensible for the algorithm.
      In both cases, the decision should be clearly documented, both in the source and the help docs for the UGen.
  • Triggerd inputs should default to their “untriggered” state (usually the value 0) so as to
    be ready for a trigger on the first sample (at time 0);
  • If using your calculation function to calculate the ZOUT0(0), you must restore
    your unit’s state properly so that this state will occur again when calculating the first sample.
    • IOW, as shown above in the SinOsc fix, if your calculation function advances the state of member variables to time y(1), you need to follow up your call to next(1) in your constructor with resetting member variables to the state before calling next(1) .

Note my suggestion that addresses your initial question:

For units requiring values for y(-1), y(-2)…

Somewhat discouragingly, I didn’t get much response. I’ve since started to undertake the task of fixing the huge number of UGen initialization problems, which will take a long time, but which I’ll do batches.

At least in the case of Impulse , I believe [/supercollider/supercollider/pull/4150][supercollider/supercollider/pull/4150)fixes the presample issue, along with a bunch of other impulse bugs and problems.

Hm. We should wake that PR back up. Seems it’s been forgotten.

Please do!

Sorry to harp on this, but because of the dozens of initialization bugs and confusion about what an initial sample is vs. the first sample, triggers on the first sample often fail. This is documented both in the previous link I sent, as well as in the PR I submitted for the Impulse overhaul that @scztt linked to above.

[Apologies if this post is a duplicate, my original was hidden]

I’ve looked into this in great detail already and documented it HERE, including summarizing where the discussion was left.

Here’s what I proposed for UGen initialization going forward:
(I had to scrub most links out because of a link limit, see original post if you’re interested)

I’ll summarize what I’ve gathered from [#2333], [#2343] and propose these guidelines for initializing UGens:

  • The initialization sample and the first sample should be equal and both represent the unit’s
    output at time 0, y(0). Make no assumptions about how your signal will be used downstream (e.g. as a trigger).
  • If the initialization sample represents anything other than y(0), it needs to be clearly documented as and exception to the rule, what it is and why.
  • For units requiring values for y(-1), y(-2), etc, it’s the author’s responsibility to
    • a) provide an interface to them. I’d consider this best practice: optional arguments to the UGen to directly provide these values.
      • See the zi argument in scipy.signal.sosfilt , and
      • the skip argument in CSound’s tone .
    • or b) set these manually with values that are sensible for the algorithm.
      In both cases, the decision should be clearly documented, both in the source and the help docs for the UGen.
  • Triggerd inputs should default to their “untriggered” state (usually the value 0) so as to
    be ready for a trigger on the first sample (at time 0);
  • If using your calculation function to calculate the ZOUT0(0), you must restore
    your unit’s state properly so that this state will occur again when calculating the first sample.
    • IOW, as shown above in the SinOsc fix, if your calculation function advances the state of member variables to time y(1), you need to follow up your call to next(1) in your constructor with resetting member variables to the state before calling next(1) .

Note my suggestion that addresses your initial question:

For units requiring values for y(-1), y(-2)…

Somewhat discouragingly, I didn’t get much response. I’ve since started to undertake the task of fixing the huge number of UGen initialization problems, which will take a long time, but I’ll do it in batches.

At least in the case of Impulse , I believe [/supercollider/supercollider/pull/4150][supercollider/supercollider/pull/4150)fixes the presample issue, along with a bunch of other impulse bugs and problems.

Hm. We should wake that PR back up. Seems it’s been forgotten.

Please do!

Ok, I can imagine some units set the initialization sample to y(0) (for a
trigger producer, 1) and then _next advances to y(1) for the first “real”
output (for a trigger, 0). Yes, that’s obviously a bug. We would not expect
any downstream unit to fire on an init-sample trigger that is negated by
the first “real” output.

I was responding to the concern that a nonzero init sample would block a
nonzero first sample from being recognized as a trigger. This is not a
concern because prevtrig should always initialize to 0.

I think you’re referring to the case where a trigger producer doesn’t
actually output 1 for its first true sample, which is a quite different case.

hjh

https://www.mobisystems.com/aqua-mail

No, I’m referring to the trigger receiver, though the sender and receiver are linked, so it gets tricky. I’ll try to clarify (again, my filed issue does a more thorough job of it).

To clarify terms, what I call the initialization sample is what is set to OUT0 in the course of executing the ctor (whether directly in the ctor or by calling next(1) from the ctor). In the context of the ctor, this is setting y(0).

The first sample, is the first sample calculated along with the rest of the control block once the UGen runs: next(64). This also writes to OUT0, and is therefore also y(0). This is why the first rule in my proposed initialization spec is “initialization sample and the first sample should be equal”.

The important point is that by calling next(1) in the ctor, you’re not only setting OUT0, you’re also advancing the state of any member variables that are altered in next(). For triggered UGens, that variable is prevtrig.

When a trigger is received on x(0), next(1) will receive that trigger, set y(0) accordingly, and set prevtrig == 1. For the next calculation cycle, prevtrig <= 0 && trig > 0 will evaluate to false thus preventing the trigger from occurring on the next cycle. So the first cycle of next(64) will not trigger, and overwrite OUT(0), invalidating the actual triggered input.

The solution to this is to be sure that if the ctor resets the state of the UGen after calculating the initialization sample so it behaves correctly when calculating the “real” output in next(64).

This is getting a bit OT for this post, I’m happy to carry on this discussion in a new thread, or better yet, the original issue.

I had a look a few times, and didn’t understand it well enough to use it. I’ll have to have a closer look next time as it does sound like a very robust solution to a lot of problems I’ve been having.

OK, I understand now.

hjh