Mixing kontrol rate and audio rate in a same UGen

In Faust the controllers are defined with this syntax:
https://faustdoc.grame.fr/manual/syntax/#user-interface-primitives-and-configuration

The current Faust <==> UGen C++ code is only handling/connecting with Faust input controllers (button, sliders, nentry in Faut syntax…), see in particular this Faust_updateControls code:

which basically uses the IN0 macro to access each controller and update its value, one per audio cycle:

Up to now, output controllers (bargraph in the Faust model) where not handled. So following the input model, I tried to do the same in this new version, handing input controllers with the IN0 macro and in this renamed Faust_updateInputControls:

and this new Faust_updateOutputControls function using the OUT0 macro here:

To be used at each audio cycle :

But Christof Ressi answered me:

As I explained, a UGen can only have one computation rate (audio or
control). So if your UGen already does audio computations, all outputs
must be audio rate. As I said, you could use audio outputs for control
information, but you would waste lots of space. Every output would be a
whole audio buffer, but you only write a single sample. (The remaining
samples need to duplicated or zeroed, btw!) If you have, say 100
controls, the UGen would essentially allocate 100 buffers! Also keep in
mind that your UGen is defined with the
kUnitDef_CantAliasInputsToOutputs, which means that it can’t reuse
existing buffers.

I would definitely look into using control busses or Buffers for output
controls.
I would recommend to get some more opinions on this, especially by Josh
Parmenter and Scott Carver.

So my current understating is that using this IN0 and OUTO model is not appropriate at all.

What are Josh Parmenter and Scott Carve opinion on how the whole input/output controller model should be reworked ? Any UGen example code to look at ?

Thanks.

To be clear, the IN0 model is totally fine, it’s only the outputs that are problematic!

Personally, I tend towards control busses because they can be allocated synchronously in the Client. Buffer allocation, on the other hand, is asynchronous and would require to go through the NRT thread before the UGen could use it.

Pinging @josh and @scztt for advice.

1 Like

@Spacechild1 I’m still not clear why the IN0 is OK. Doesn’t this also allocates a complete buffer and use only the first index to store the control value ?

Doesn’t this also allocates a complete buffer and

UGens don’t need to allocate buffers for inputs, they just point to the output buffers in the upstream UGen. For control rate UGens, this output buffer is just a single float.

Note that UGens don’t really own their output buffers, instead these are “wire buffers” that are shared between UGens. But if you need dozenzs or hundreds of them in a single UGen, these buffers still need to be allocated and consequently occupy memory.

Scsynth uses global wirebuffers for all Synths. In Supernova, however, Synths may be processed in parallel, so they all have their own set of wire buffers!

What number of control outputs are we talking about? Is there an upper bound?

To be clear, the problem is not the number of outputs per se, but the fact that these need to be audio rate (because the whole UGen is audio rate).

This ofc makes all the difference. :slight_smile:

In general, I wouldn’t worry too much about this unless e.g. the UGen is the sort of thing that will be used N times in a single Synth / in N synths at once. These days I generally use ONLY audio-rate for all UGens, and only switch to control rate for targeted optimization if my CPU load is too high - it’s very uncommon (though not unheard of) that I see CPU usage problems worth caring about that can be resolved by reducing the number of wirebufs (which is what we’re mainly talking about here: the overall memory footprint of a Synth, and how it related to CPU cache size). Christof is correct, kUnitDef_CantAliasInputsToOutputs is important here if you can use it.

My impression is the GENERAL use-case for Faust is to build complex DSP pipelines for things like reverbs and synths. These are - especially in cases where they would have multiple outputs! - probably a one-per-Synth sort of UGen. The overhead of having multiple audio-rate outputs for a moderate-to-high complexity synth like this should be low, as long as you’re talking about 2 or 5 or 12 (but maybe not 50) outputs. All of this should be empirically tested to see the real-world impact - performance profiles of cache and memory related things like this can be very delicate, so careful real world performance testing across multiple machines is the only metric that matters.

Something critical here is to efficiently write to your outputs, especially for cases where you e.g. only produce one output value per block. As Christof suggested, you’ll either need to duplicate this value or ramp it - either way, you’ll want to use vector instructions to do this. Abstractions for both writing single values and ramped values should already existing in the nova package, and probably very simple for loops will get vectorized automatically, but you’ll want to use something like compiler explorer to be sure.

FWIW, if you wanted to produce e.g. 1 audio output channel plus 64 control rate outputs, you might be able to do this in two stages:

  • one faust UGen that writes the audio output, plus all control outs to successive samples of another audio buffer
  • one UGen that reads a single audio range input, and splits it out to N control rate outputs
MyFaustUgen {
  *ar {
      var audio, extraSigs, result;
      result = MyFaustUgenImpl.ar();
      audio = result[0];
      extraSigs = result[1..]; // ar signals packed with kr data
      extraSigs = ControlRateSplit.kr(extraSigs, numOutputs=MyFaustUgenImpl.numControlOuts);
      ^[audio] ++ extraSigs
  }
}

These two ugens could be chained (in sclang) in a way that is hidden from user code, so the user-facing ugen would just return an audio rate signal plus N control rate signals as expected.

I’m not 100% sure this would work, but it’s worth an attempt if the use-case really entails a super large number of control-rate output channels.

That’s a cool idea, but I don’t think it would work. If you want N controls with a blocksize of B, you would need ceil(N / B) audio outputs. When building the SynthDef, the number of audio outputs must be fixed, but you cannot know the blocksize yet! (By default, the blocksize is 64 samples, but sometimes people use higher or lower blocksizes!)

The overhead of having multiple audio-rate outputs for a moderate-to-high complexity synth like this should be low, as long as you’re talking about 2 or 5 or 12 (but maybe not 50) outputs.

Yes! Stephane mentioned to me that he wants to output the grain positions in an ambisonic granular synthesis patch, so I assumed that he would need at least a few dozens control outputs, if not more.

The graph file format has a slot for the calculation rate of each output of each Ugen, as well as a slot for the calculation rate of the Ugen itself, suggesting you can have control-rate outputs at an audio-rate Ugen?

Cf. ugen-spec and output-spec at:

https://doc.sccode.org/Reference/Synth-Definition-File-Format.html

It’s possible this actually works?

I suspect the only way to know is to try it.

A multichannel UGen class (the sclang side) should return an array of OutputProxies. I think in all current UGens, these are all the same rate, but they’re distinct objects so it would be possible to create an output array with different rates for different elements.

I don’t know how scsynth will instantiate the UGen though.

hjh

Thanks for all the answers. Any C++ UGen example code I should look at to start with ?

This is very interesting, I was not aware of this! Thanks a lot for bringing this up!

The Class Library doesn’t currently support this, at least not directly, because MultiOutUGen.initOutputs uses a single rate argument for all outputs. You could, however, write your own initOutputs method that supports different rates for the individual outputs.

But here’s the big catch: scsynth and supernova do not actually support different output rates! They do store the actual rates in the Unit output specs, but when they instantiate a Synth, the calc rate of each output wire gets bashed to the Unit calc rate:

Note L.429 and L.438!

Note L.85!

As a consequence, the UGen would crash if the Unit calc rate was calc_FullRate, but one or more outputs are specified as calc_BufRate or calc_ScalarRate. This is because in the UGen spec the buffer index is set to -1 for non-audio wires, so Unit_new would assign bogus audio buffers! (Supernova would at least abort in debug mode with the failed assertion in L.92.)

I guess the behavior of Scsynth and Supernova could be changed, we’d just need to be careful about backwards compatibility. In particular, in the Client implementation you would only be able to mix different output rates in recent enough SC versions. This can be done with a simple runtime version check. You just have to make sure that you don’t accidentally use a newer SynthDef in an older SC version.

I’ll open a ticket on GitHub and – if I have time – also a PR.


@Stephane_Letz For you this would mean that you could use regular (audio-rate) Ugen outputs, hoping that you would be able to use proper control outputs in the future. For users it shouldn’t make a difference, they should just assume that the outputs are control-rate.

Thanks again, but still: is there any C++ UGen example I could look at for now, even its is to optimal ?

As I said, you can just write your controls to the audio outputs, just make sure to either duplicate the value over the whole buffer or zero the remaining samples. (The first one is probably better.)

But first I’d wait until we check if the current behavior of scsynth/supernova can be changed in the future (without breaking backwards compatibility). If not, I’d rather opt for the control bus solution, depending on the number of controls.

BTW, you didn’t answer how many control outputs you actually need (min./max.)

On Sunday I need to spend a few hours in the airport. I’ll try to have a look!

Something like 32 control outputs maximum.

Looking again the the current Faust supercollider architecture file (that was written like 15 years ago by Stefan Kersten and that I am now trying to understand…)

Something like 32 control outputs maximum.

Ok, in this case I’d say go for regular (audio) outputs.

So does this mean the code was ready for possible mix in AR and KR ?

I am not sure I understand. Input controls can have different rates, it’s just outputs that (currently) must all have the same rate.

Regarding interpolation: you may also decide to interpolate the outputs, but personally I am not sure it would make sense. First, we actually want these outputs to become “real” control outputs in future SC versions. Second, interpolation is typically done on the inputs, depending on the Ugen in question. (For some inputs, interpolation does not make sense in the first place.) Finally, if required, interpolation can also be done explicitly by the user with various Ugens, e.g. .lag.

To make the code future proof, you could check if the “control” outputs are audio rate and if yes, you would duplicate the value; otherwise you only need to write a single value with OUT0.

and @Spacechild1 if we can work together on the C++ code, this would help, possibly interacting on Discord

I am pretty busy at the moment, but we could arrange for a short meeting next week or so. Just write me an e-mail.

Latest version of the glue code:

You need to add them. In general, UGens only have inputs and outputs (with individual rates); whether these are “audio inputs/outputs” or “control” is just a matter of interpretation. So if you want M “audio outputs” and N “control outputs”, you need to create M + N Ugen outputs.

// The control outputs are not part of unit->mNumOutputs ?

They certainly are! I think you need to take a look at the client code that is responsible for writing the UGen spec in the SynthDef file.