[WIP] FaustGen - a UGen for interpreting Faust code

Hello all
Inspired by comments in the gettings started with Faust for SuperCollider thread, I’ve slowly started work on a UGen that will enable us to interpret Faust Code in a SuperCollider UGen. I’ll use this thread to post updates about the project. If anyone wants to help out with this project I would love that - even just comments/ideas posted here would be nice.

Get notified of the first release
Once the project is stable enough for general use, there will be prebuilt binaries available on the Github page. Click “watch” on the github repo to be notified when this happens.

Source code is here:

Resources:

8 Likes

If anyone has alternive strategies for communicating faust code strings from the language to the server, please let me know. For now I will try the ByteBeat approach mentioned above but let me know if there are other nice ways of doing this!

Unit commands are a viable solution. However, there a couple of caveats, as described in sc/src/VSTPlugin.cpp · master · Pure Data libraries / vstplugin · GitLab.

Working with Unit commands is also awkward because obtaining the synthIndex of specific UGen in a SynthDef can be quite tricky and full of edge cases. See the sclang client code of VSTPlugin for an example.

As an alternative, you could use a plugin command instead. On the SC side, the FaustUGen could take an arbitrary ID as an argument. When you create the UGen on the Server, it would store itself in a dictionary, using its node ID + custom ID as the key. The plugin command would take the node ID + custom ID as the first two arguments, so it can look up the UGen instance in the dictionary or - if the constructor hasn’t run yet - save the data for later.

The dictionary itself should be realtime safe, though, so make sure to use RTAlloc/RTFree. IIRC, in the plugin API headers there’s already a RT allocator class that you can plug into a std::unordered_map. (Otherwise, you can try mine: sc/src/rt_shared_ptr.hpp · master · Pure Data libraries / vstplugin · GitLab). Alternatively, you can just use a simply dynamic array with a linear (or binary) search.

BTW, if you want to make the UGen realtime safe, the parsing should happen on the NRT thread, in which case you would need to perform an asynchronous command. Beware that the UGen instance can get destroyed under your feet while the command is running. Here the dictionary solution helps again, because you can simply try to look up the UGen instance once the command has succeeded. This is much better than having a direct pointer to the UGen, because you can’t know if it is still valid. (VSTPlugin uses unit commands + complicated reference counting logic, but in hindsight the plugin command + dictionary solution might have been easier…)

I have finally opened a ticket for the unit command problem: unit commands executed before unit initialized · Issue #5488 · supercollider/supercollider · GitHub

Thanks for the pointers! This is really great. I am trying the plugincmd approach now using this as a reference: supercollider/UIUGens.cpp at 18c4aad363c49f29e866f884f5ac5bd35969d828 · supercollider/supercollider · GitHub

I have more questions about your suggestions but will save them for later once I’ve tried this out. Thanks !!

Thanks to your wonderful suggestions @Spacechild1 we’ve now got a very basic live codeable faust compiler going!

Here’s a simple showcase to prove it’s alive :slight_smile:

There’s still a lot of work left to do but the basic idea is starting to take shape.

9 Likes

Have a look also at Albert Graef reworked/improved pd-faustgen

Yes I’ve used that one as a reference and it was really helpful in setting up the cmake stuff. Very cool!

The next big problem I am facing now is how to handle differnt numbers of inputs and outputs.

When the UGen starts it has a fixed number of outputs and inputs. In some cases this is okay: Let’s say the FaustGen ugen has 10 outputs, then the faust code may create up to 10 outputs as it is now. If it produces more than that, it is rejected by the parser.

The harder problem I find is how to deal with inputs. Here the same goes - the parser allows the faust code parsed to have a number of inputs up to the number of UGen inputs.
But the FaustGen UGen has one input for the id mentioned by @Spacechild1 above to make it identifiable on the server. What I’d like to do is only copy the input buffers after that input index to the faust compute function. But does anyone have ideas on how to deal with this? Here’s what I’ve tried (wich makes the server crash)

void FaustGen::next(int nSamples) {

  FAUSTFLOAT **faustInputs;

  /* Remove inputs used by the UGen at init */
  for (size_t in_num; in_num < mNumAudioInputs; in_num++) {
    const auto offset = mNumAudioInputs;
    faustInputs[in_num] = this->mInBuf[in_num + offset];
  }

  // compute faust code
  m_dsp->compute(nSamples, faustInputs, (FAUSTFLOAT **)this->mOutBuf);
}

Any input welcome!

wich makes the server crash

I’m not surprised :slight_smile:

FAUSTFLOAT **faustInputs;

Where do you assign this variable?

const auto offset = mNumAudioInputs;

This looks wrong. The offset should rather be the number of reserved UGen inputs.

Anyway, assuming that you reserve N inputs (e.g. for the ID), you can simply do the following:

m_dsp->compute(nSamples, (FAUSTFLOAT **)mInBuf + N, (FAUSTFLOAT **)mOutBuf);

BTW, the code above only works if FAUSTFLOAT is defined as float. You can add a static assertion to make sure:

static_assert(sizeof(FAUSTFLOAT) == sizeof(float), "FAUSTFLOAT must be float");

1 Like

I’ll add more comments in GitHub

1 Like

fantastic. Thank you!

It’s a really nice project! I always thought about what could happen if Faust was usable from the SC ecosystem and it is nice to see it take shape. Thanks Mads!

1 Like

Yes, very nice… several tries in the past… but this new one seem to be the good one :sweat_smile:

I’m very excited about this project!

Just to throw an idea for consideration. It seems that you are thinking of the ugen as a dynamic faust interpreter, one ugen instance executes many different faust programs. The approach I would have imagined for sc was different ugen instances executing only one faust program each, referenced by name and preloaded asynchronously. Like loading ugens the same way synthdefs are preloaded. And that could be very powerful, ugens can be created dynamically like synthdefs, the whole server may be set to run only faust ugens, ta-dah!. Also this way, instead of having a controller, the ugen is ready when the synthdef graph is built. That might even help with the inputs/outputs problem.

This is a great idea Lucas. the Faust API actually is pretty much made like this - it has a DSP factory that is made from the (sucessfully parsed) faust code and then you can spawn dsp computing objects from that so I think this is a very reasonable scenario for us!

I’ve pushed some more code today trying to seperate as much as possible of the parsing into the NRT thread.

I’ve also added a TODO list with the things we need to solve before this is good to go. I’ll soon close up shop for the summer so if anyone’s interested in working on any of these problems while I’m away from the keyboard let me know - PR’s are very very welcome!

You can already statically compile Faust DSP code to ugen using the faust2supercollider tool. The point of this FaustGen project is more of a dynamically programmable ugen AFAICS.

It would be dynamic the sc way, the server would be running and creating an ugen will be part of creating a synthdef. The difference would be in the entry point/api. I can see (dis)advantages in both approaches.

The controller approach as shown is much more direct for live coding Faust, and that is a good use case, but that implies that the synthdef graph cannot change, the dynamic entry point is the ugen, hence the input/output problem. The other way, the graph changes with the ugen which is more common in sc, the dynamic entry point is the synthdef. Regarding the control, the first approach has to set the data after a synth is created and to have two ways to set the synth parameters, /n_set for node controls and /u_cmd for ugen code. The second approach has to set the data before and the node control is as usual.


// first, better for live coding faust with f.eval

SynthDef(\test, { args otherParam; Out.ar(0, FaustGen.ar(1, otherParam)!2 * 0.5) }).add; // async

z = Synth(\test); // sync

f = FaustGenController.new(z,1);
f.eval(faustCodeOrFile); // async

z.set(\otherParam, 123); // sync

// second, I thought more idiomatic

s.sendMsg(\cmd, \load_faust, faustCodeOrFile); // async
SynthDef(\test, { args otherParam; Out.ar(0, FaustGen.ar('faustCodeName', otherParam)!2 * 0.5) }).add; // async

z = Synth(\test); // sync
z.set(\otherParam, 123); // sync

Synchronous commands are important for precise timing and each approach has a different order, that is something to consider too. I think booth approaches are complementary for different use cases. Side note, before Spacechild1 I can’t remember anything that uses /u_cmd (neither /cmd in plugins to be fair), it looks like a powerful forgotten feature to me.

1 Like

I agree and I’ve already suggested it to Mads (in private). I think, both methods you have mentioned above have their merits and should be supported.

Yeah I agree - this is something we should have.