After my Physical Modeling Flute Faust experience I decided to try to model a virtual analog synthesizer in Faust. I looked at several synth structures and came up with this fairly standard wiring :
2 LFOS, 2 Oscillators, Noise generation, 2 Filters, 2 Amplifiers and 4 ADSR envelopes (integrated in Filters and Amplifiers).
Designing this in monophonic form was fairly easy, as no recursion in top level elements was needed like in many physical modeling designs. For oscillators, filters and envelopes standard components of the Faust libraries were used. Finishing a monophonic synth was easy.
Changing the monophonic synth into a polyphonic one was also quite straight forward. The single gate, gain and pitch controls for a single voice needed to be duplicated. I moved the original process declaration into a single voice with the following definition
voice(gate, gain, pitch) = (lfo1, lfo2) <: (_,_,osc1,osc2,noisegen)
: (_,_,pre_filter_mix) // l1, l2, f1_in, f2_in
// to filters
<: ((_,_,!,_), ((_,!,_,!) : filter1)) // l1, l2, f2_in, filter1, filter1_to_f2
<: ((_,!,!,_,!), (!,_,!,!,!), ((!,_,_,!,_) : (_,_+_) : filter2)) // l1, f1_out, l2, f2_out
// to amps
: (amp1, amp2)
with {
// ...
// in : o11, o12, o21, o22, n1, n2
// out : f1_in, f2_in
pre_filter_mix = bus6 <: (((_,!,_,!,_,!) : _+_+_), ((!,_,!,_,!,_) : _+_+_));
};
The new process looks like this
process = par(i, 8, voice(i)) :> (_,_,_,_) : effects :> (main_out * _, main_out * _);
8 voices are run in parallel and are then mixed into a 4 channel signal which is fed to the effects section and then fed to Stereo out.
A problem with this design is that all the 8 voices are always on. And the resulting Faust structure is quite large and takes a long time to compile. With several compiler level optimizations this design can be made runnable.
Other performance issues are on a lower level with select2 and select3 constructs, which are Faust’s if-else equivalents. Non-active branches can only be skipped in Faust if they have no memory. Memory, or state, is involved if you use any kind of delays, either explicitly or via usage of recursion. The “always on” philosophy needs to be taken into account on every level, if low CPU usage is a goal.
I defined a single multi-waveform oscillator like this
oscillator(type, freq, width) = select5(type,
osc(freq),
triangle(freq),
sawtooth(freq),
squarewave(freq, width),
random);
As all of these components use state internally to keep track of the current phase all of them are always run.
One way to fix this is to separate the phase which is stateful and needs to be memorized from the wave shape generation. This is another oscillator design which has stateless functions in the wave selection :
oscillator(type, freq, width) = phase(freq) : phase_to_osc(type, width);
phase(freq) = (+(q) : mod1) ~ _
with {
q = float(freq)/float(SR);
};
phase_to_osc(type, width) = _ <: select5(type,
sin(2*PI*_),
tri,
saw,
square(width),
osclib.noise);
square(w) = select2(_ < w, -1.0, 1.0);
saw = bi;
tri = bi : fabs : bi;
bi = 2.0*_ - 1.0;
mod1 = fmod(_, 1.0);
phase is the phase generation and phase_to_osc is the wave shaper.
I will probably use this kind of phaseshaping oscillator structure also for my coming synth projects, Faust and non-Faust, because it is really flexible. More about this approach is explained in this paper : Phaseshaping Oscillator Algorithms for Musical Sound Synthesis
With the multi-mode filter there is a similar issue. My current naive form looks like this
reson_filter(type, freq, res) = _ <: select4(type,
resonlp(freq, res, 1.0), // lowpass
resonhp(freq, res, 1.0), // highpass
resonbp(freq, res, 1.0), // bandpass
resonbr(freq, res, 1.0)) // bandreject
with {
resonbr(fc,Q,gain,x) = (gain * x) - resonbp(fc,Q,gain,x);
};
I have experienced a little bit with Moog ladder filter designs in Faust, but nothing really properly working has come out yet.
This synth is again wrapped as LV2 synth plugin with a GTK+ gui. I used Cairo to draw the knobs. Here is a screenshot :
The code is hosted at GitHub, so take a look if you are interested. I do not plan to maintain this project much, as I don’t see Faust in it’s current form the right tool for virtual analog synth design. Things I intended but didn’t manage to do are listed in the Issues section.
Polyphonic operation inside Faust is slow and I expect modulation matrices, alternative wirings and unison modes to be difficult to implement. Faust works quite well though for monophonic synths with a fairly static wiring, since the automatic SVG generation from Faust code makes such structures much more accessible than code.
Flauta is a project I am currently working on. It is a port of a STK based Flute model by Patricio de la Cuadra to Faust. Flauta is monophonic, uses a complex but static structure and most of the elements are mandatory, so Faust is a great match. I will write more about Flauta when we get some proper releases done.
Have you tried similar things with Faust? I am interested to hear more about Synth projects with Faust.
Concerning Virtual Analog in Faust, there is also the foo-yc20 project by Sampo Savolainen, which is a virtual model of the Yamaha YC-20 organ. It also uses a polyphonic design on the Faust level and has a much nicer GUI than mine 😉