Exporting a UGen to a VST?

Hi there -

I really love the DEIND extensions for SC. I particularly like the “DiodeRingMod” - and I’d like to use it in my DAW as a VST or an AU.

Since the source code is available, I’m curious how difficult it would be to translate into something like FAUST. There’s not a ton of information on how to go about doing something like that - I’m assuming it is not a trivial amount of work - but I’d be interested to understand the process a little better, if anyone here has done that kind of thing.

Thanks again.

1 Like

It will not be too complex, even as your first Faust experiment. At first glance, you must do three key things: initialization, rewriting the transfer functions, and the ring-modulation process loop.

Initialization (original)

 unit->diodeCurveCoeff = unit->h/(2*(unit->v_l-unit->v_b));
 unit->diodeLinCoeff = unit->diodeCurveCoeff * pow(unit->v_l - unit->v_b, 2) - unit->h * unit->v_l;

Which may be something like this? (I would double-check operator precedence in c++ and faust, I’m not sure they match)

diodeCurveCoeff = h/(2*(v_l-v_b));
diodeLinCoeff = diodeCurveCoeff * ((v_l - v_b)^2) - h * v_l;

Then, you will need to rewrite the transfer function (you can use ba.if or select3, for example).

A variation would be a simple function, not discrete ( if/else logic) as the original (I’m not sure this will be equivalent, but would be interesting to test):

diode_transfer(input) = 
    (input > v_b) * (diodeCurveCoeff*(input-v_b)^2) +
    (input > v_l) * (h*input + diodeLinCoeff - (diodeCurveCoeff*(input-v_b)^2));

And the dsp loop process, in which this part can be rewritten almost as-is:

// c++: float in1 = carrier[i] + 0.5*modulator[i];
in1 = c + 0.5*m; 

// c++: float in2 = carrier[i] - 0.5*modulator[i];
in2 = c - 0.5*m;   

Also:

diode_process(input) = diode_transfer(input) + diode_transfer(input * (-1));

Let me know if any of this makes sense.

1 Like

@wheeler Have you tried it?

I just noted one thing. I exposed three parameters that were hidden in the original UGen version. That is good in itself; you can experiment more. But you must add an extra line to prevent division by zero, avoiding nasty glitches. Besides that, I think this version has no issues and three extra parameter controls.

declare name "DiodeRingMod";
declare version "0.1.0";
declare author "Original SC UGen by Julian Parker & Till Bovermann";
declare license "GPL3";

import("stdfaust.lib");

v_b = hslider("break_voltage [osc:/diode/break_voltage]", 0.2, 0.01, 1.0, 0.01) : si.smoo;
v_l = hslider("linear_threshold [osc:/diode/linear_threshold]", 0.1, 0.01, 1.0, 0.01) : si.smoo;
hs = hslider("linear_slope [osc:/diode/linear_slope]", 1.0, 0.1, 2.0, 0.01) : si.smoo;
amp = hslider("amp [osc:/diode/amp]", 0, -60, 20, 0.1) : ba.db2linear : si.smoo;

no_zero(x) = ba.if(x == 0, ma.EPSILON, x);
v_diff = v_l - v_b ;

dcc = hs / (2 * (v_diff : no_zero));
dlc = dcc * (v_diff^2) - (hs * v_l);

dt(x) = ((x > v_l) * ((hs * x) + dlc)) 
      + ((x > v_b) * (x <= v_l) * (dcc * (x - v_b)^2));

dp(x) = dt(x) + dt(-x);
mc(c, m) = dp(c + (0.5 * m)), dp(c - (0.5 * m));

process = ( _ , _ ) : mc : ( _ - _ ) : *(amp) : ef.softclipQuadratic;
1 Like

Not yet - there are a couple of things that I might need to do a little follow-up on :slight_smile:
But thank you for showing how this would be done, it completely illuminates something that seemed impossible just a few days ago.

1 Like

This last code is the complete implementation. That’s all the code. I am curious to know if it is equivalent to the UGen. Glad to help!

EDIT: This seems better regarding waveforms; this code returns a waveform with more symmetry or balance between positive and negative samples, which I believe is preferable.

declare name "DiodeRingMod";
declare version "0.1.0";
declare author "Original SC UGen by Julian Parker & Till Bovermann";
declare license "GPL3";

import("stdfaust.lib");

v_b = hslider("break_voltage [osc:/diode/break_voltage]", 0.2, 0.01, 1.0, 0.01) : si.smoo;
v_l = hslider("linear_threshold [osc:/diode/linear_threshold]", 0.1, 0.01, 1.0, 0.01) : si.smoo;
hs = hslider("linear_slope [osc:/diode/linear_slope]", 1.0, 0.1, 2.0, 0.01) : si.smoo;
amp = hslider("amp [osc:/diode/amp]", 0, -60, 20, 0.1) : ba.db2linear : si.smoo;

no_zero(x) = ba.if(x == 0, ma.EPSILON, x);
v_diff = v_l - v_b ;

dcc = hs / (2 * (v_diff : no_zero));
dlc = dcc * (v_diff^2) - (hs * v_l);

dt(input) =
    ba.if(input > v_l,
        (hs * input) + dlc,        //linear positive
        ba.if(input > v_b,
            dcc * (input - v_b)^2, // quadratic positive
            ba.if(input < -v_l,
                (hs * input) - dlc, //  linear negative
                ba.if(input < -v_b,
                    -dcc * (input + v_b)^2,  // quadratic negative
                    0               // otherwise cut-off, 0, silence
                )
            )
        )
    );

mc(c, m) = dt(c + (0.5 * m)), dt(c - (0.5 * m));

process = ( _ , _ ) : mc : ( _ - _ ) : *(amp) : ef.softclipQuadratic;

It may not be the original model, but that’s the above transfer function and its parameters. The UGen had hard-coded values for them; you can play with that, too. It seems much more interesting this way, I think.

The idea (from the UGen code) is to split the function into three regions: all-zero, linear, and quadratic, but always the same values, but there is no reason not to control at runtime