One normally never needs to look at the C++ code generated by FAUST. However, we will do this now just to see how it looks, and note a few things.
Running FAUST with no architecture file, e.g.,
> faust cpgr.dspcauses the C++ signal-processing code to be printed on the standard output, as shown for this example in Fig.9.
class mydsp : public dsp { private: float fConst0; float fConst1; float fVec0[3]; float fConst2; float fRec0[3]; public: static void metadata(Meta* m) { } virtual int getNumInputs() { return 1; } virtual int getNumOutputs() { return 1; } static void classInit(int samplingFreq) { } virtual void instanceInit(int samplingFreq) { fSamplingFreq = samplingFreq; fConst0 = expf((0 - (314.1592653589793f / float(fSamplingFreq)))); fConst1 = (2 * cosf((6283.185307179586f / float(fSamplingFreq)))); fConst2 = (0.5f * (1 - faustpower<2>(fConst0))); for (int i=0; i<3; i++) fVec0[i] = 0; for (int i=0; i<3; i++) fRec0[i] = 0; } virtual void init(int samplingFreq) { classInit(samplingFreq); instanceInit(samplingFreq); } virtual void buildUserInterface(UI* interface) { interface->openVerticalBox("cpgr"); interface->closeBox(); } virtual void compute (int count, FAUSTFLOAT** input, FAUSTFLOAT** output) { FAUSTFLOAT* input0 = input[0]; FAUSTFLOAT* output0 = output[0]; for (int i=0; i<count; i++) { float fTemp0 = (float)input0[i]; fVec0[0] = fTemp0; fRec0[0] = ((fConst2 * (fVec0[0] - fVec0[2])) + (fConst0 * ((fConst1 * fRec0[1]) - (fConst0 * fRec0[2])))); output0[i] = (FAUSTFLOAT)fRec0[0]; // post processing fRec0[2] = fRec0[1]; fRec0[1] = fRec0[0]; fVec0[2] = fVec0[1]; fVec0[1] = fVec0[0]; } } }; |
We see that init calls classInit, which is where read-only wavetables are initialized (none being used in this example), followed by instanceInit, which resets all parameters to their default values. Thus, instanceInit provides a more efficient processor ``reset'' when readonly wavetables are in use.
Since all processor state is allocated as instance variables of the mydsp class (which can be changed to any name using the -cn FAUST-compiler option), there is no allocation in init.
Notice how constant subexpressions, such as for fconst0, are computed only once in instanceInit. The template faustpower<2>(x) (omitted in the above listing) expands to x*x, thereby avoiding calling the pow function. In general, FAUST does a lot of such optimization.
The buildUserInterface method calls the appropriate interface function for each control widget (slider, button, etc.), but there are none in this simple example. In §5 we will add GUI controls, and you compile that to see how buildUserInterface changes as a result. The GUI control variables are also included among the processor state variables, and the interface is given pointers to them. (The interface holds no signal-processing state, including both signal and controller values.) The interface may update the control variables asynchronously (e.g., in another thread of execution), and they will get sampled in the signal processor once per execution of the compute inner loop. Thus, the control rate is the sampling rate divided by the audio buffer length count. As a result, elaborate FAUST expressions in the control variables are normally very inexpensive computationally. For optimization, we tend to look hard only at the for loop in the compute function; for example, we generally try to avoid calls to libc for things like sin() and cos(), which can be relatively slow. There are fast approximate alternatives such as the fastapprox library,14and linearly interpolated lookup tables are often used.