Implementing a single sample feedback oscillator

Hello,

I have been trying to recreate a sort of oscillator from a SVF filter model which relies on single sample feedback. Trying the thing out first using DynGen, I can get some sound out with the following code:

(
DynGenDef(\myosc, "
@init
md1 = 0;
mc2 = 0;
p_md1 = 0;
p_mc2 = 0;
@sample
p_md1 = wrap(md1, -1, 1);
p_mc2 = wrap(mc2, -1, 1);
md1 = _a1 != 0 ? p_md1 + _a2 - (0.1 * p_mc2) : p_md1;
mc2 = p_mc2 * (_a4 + 1) + (0.49 * _a3 * p_md1);
out0 = p_md1;
out1 = p_mc2;
").send;
)

(
u = SynthDef(\unstable, {
    | in1, in2, range2, in3, range3, in4, range4, clock_mod, bus_in, bus_out |

    // 1
    var oscn2_in1 = MyChange2.ar(
        LFPulse.ar(
                            1000 /  Ramp.kr(Ramp.kr(in1.linlin(0, 127, 0, 1000), 0.002).linlin(0, 1000, 1, 0.05), 0.02), width: 0.5
                        )
                    );
    // 8
    var cmod = Ramp.kr(Ramp.kr(clock_mod.linlin(0, 127, 0, 1), 0.002), 0.02);
    // 9
    var osc = In.ar(bus_in, 2);
    // 2 - 3
    var pow2 = (10 ** range2);
    var oscn2_in2 = Ramp.kr(Ramp.kr(in2.linlin(0, 127, 0, 1000), 0.002).linlin(0, 1000, -1 / pow2, 1 / pow2), 0.02) + (osc * cmod);
    // 4 - 5
    var pow3 = (10 ** range3);
    var oscn2_in3 = Ramp.kr(in3.linlin(0, 127, 0, 1000), 0.002).linlin(0, 1000, -1 / pow3, 1 / pow3);
    // 6 - 7
    var pow4 = (10 ** range4);
    var oscn2_in4 = Ramp.kr(in4.linlin(0, 127, 0, 1000), 0.002).linlin(0, 1000, -1 / pow4, 1 / pow4);

    var osc_sig = DynGen.ar(2, \myosc, params: [
        a1: oscn2_in1,
        a2: oscn2_in2,
        a3: oscn2_in3,
        a4: oscn2_in4,
    ]);

    osc_sig.poll;

    Out.ar(bus_out, osc_sig);
});
)

(
~uns1 = NodeProxy.audio(s, 2);
~uns1.source = u;
Spec.add(\in1, ControlSpec( 0, 127, \lin, 1 ));
Spec.add(\in2, ControlSpec( 0, 127, \lin, 1 ));
Spec.add(\range2, ControlSpec( 0, 3, \lin, 1 ));
Spec.add(\in3, ControlSpec( 0, 127, \lin, 1 ));
Spec.add(\range3, ControlSpec( 0, 3, \lin, 1 ));
Spec.add(\in4, ControlSpec( 0, 127, \lin, 1 ));
Spec.add(\range4, ControlSpec( 0, 3, \lin, 1 ));
Spec.add(\clock_mod, ControlSpec( 0, 127, \lin, 1 ));

~uns1.setn(
    \in1,  0,
    \in2, 0,
    \range2, 3,
    \in3, 0,
    \range3, 3,
    \in4, 0,
    \range4, 3,
    \clock_mod, 0);
~uns1.gui;
)

Then with Fb1, however the values are all zeroes:

(
if (s.options.blockSize != 64) {
    s.options.blockSize = 64;
    s.quit.reboot;
}
)

(
u = SynthDef(\unstable, {
    // 1
    var oscn2_in1 = MyChange2.ar(
        LFPulse.ar(
                            1000 /  Ramp.kr(Ramp.kr(\in1.kr.linlin(0, 127, 0, 1000), 0.002).linlin(0, 1000, 1, 0.05), 0.02), width: 0.5
                        )
                    );
    // 8
    var cmod = Ramp.ar(Ramp.ar(\clock_mod.ar.linlin(0, 127, 0, 1), 0.002), 0.02);
    // 9
    var osc = In.ar(\bus_in.kr, 1);
    // 2 - 3
    var pow2 = (10 ** \range2.ar);
    var oscn2_in2 = Ramp.ar(Ramp.ar(\in2.ar.linlin(0, 127, 0, 1000), 0.002).linlin(0, 1000, -1 / pow2, 1 / pow2), 0.02) + (osc * cmod);
    // 4 - 5
    var pow3 = (10 ** \range3.ar);
    var oscn2_in3 = Ramp.ar(\in3.ar.linlin(0, 127, 0, 1000), 0.002).linlin(0, 1000, -1 / pow3, 1 / pow3);
    // 6 - 7
    var pow4 = (10 ** \range4.ar);
    var oscn2_in4 = Ramp.ar(\in4.ar.linlin(0, 127, 0, 1000), 0.002).linlin(0, 1000, -1 / pow4, 1 / pow4);

    var osc_sig = Fb1({ |in, out|
        var md1, mc2, p_md1, p_mc2;
        var a1, a2, a3, a4;

        #a1, a2, a3, a4 = in[0];
        p_md1 = out[1][2].clip(-1, 1);
        p_mc2 = out[1][3].clip(-1, 1);

        md1 = if(a1 != 0, p_md1 + a2 - (0.1 * p_mc2), p_md1).sanitize;
        mc2 = (p_mc2 * (a4 + 1) + (0.49 * a3 * p_md1)).sanitize;
        [p_md1, p_mc2, md1, mc2]
    }, in: [oscn2_in1, oscn2_in2, oscn2_in3, oscn2_in4], outSize: 4);

    osc_sig.poll;

    Out.ar(\bus_out.kr, osc_sig);
});
)

(
~uns1 = NodeProxy.audio(s, 2);
~uns1.source = u;
Spec.add(\in1, ControlSpec( 0, 127, \lin, 1 ));
Spec.add(\in2, ControlSpec( 0, 127, \lin, 1 ));
Spec.add(\range2, ControlSpec( 0, 3, \lin, 1 ));
Spec.add(\in3, ControlSpec( 0, 127, \lin, 1 ));
Spec.add(\range3, ControlSpec( 0, 3, \lin, 1 ));
Spec.add(\in4, ControlSpec( 0, 127, \lin, 1 ));
Spec.add(\range4, ControlSpec( 0, 3, \lin, 1 ));
Spec.add(\clock_mod, ControlSpec( 0, 127, \lin, 1 ));

~uns1.setn(
    \in1,  0,
    \in2, 0,
    \range2, 3,
    \in3, 0,
    \range3, 3,
    \in4, 0,
    \range4, 3,
    \clock_mod, 0);
~uns1.gui;
)

The equations are quite close in both ways of doing, and I do think I picked the right out values (out[1] to get the previously computed values)

I am thinking that in the first example with DynGen, it is possible to pass control rate inputs to the audio rate DynGen UGen, while with audio rate Fb1, the inputs are required to be audio rate as well, and said inputs behave diffrently in that case.
Using control rate Fb1 does not result in something audible, as expected.

Other examples of Fb1 generating sound by themselves do not seem to involve audio rate controls as inputs to set the oscillator parameters.

(
x = {
    var sig = Fb1({ |in, out|
        out[1] * 2 + (out[2] * 3) % 1.01
        },
        outSize: 2,
        outInit: [[3, 1], [2, 5]],
        outDepth: 3
    ).tanh * 0.2;
    LPF.ar(sig, 2000)
}.play
)

Could someone perhaps shed a light on the cause behind this?
Thanks.

1 Like

What is MyChange2? What is Fb1? These are not part of the standard library. What do they do? Are they documented somewhere? Where did you get these classes/ugens from?

Apologies, I assumed common knowledge of 3rd party Quarks used for the problem at hand.

DynGen comes from @dscheiba’s eponymous Quark to run DSP code in a manner similar to Max’s gendsp (it’s used to implement JSFX plugins in REAPER) and can do single sample processing, whereas Fb1 is provided by @dkmayer’s miSCellaneous_lib Quark as an utility UGen to do specifically single sample processing within a graph, with its documentation available here.

MyChange2 is a custom pseudo UGen I created to track variations in a signal, recreating change~ from Max/MSP as Changed did not provide a proper equivalent, defined as follows:

MyChange2 : Filter {
        *kr { arg input;
                ^HPZ1.kr(input -  LastValue.kr(input)).sign
        }
        *ar { arg input;
                ^HPZ1.ar(input - LastValue.ar(input)).sign 
        }
}

When the current sample is greater in value than the previous sample, change~ outputs a sample of 1. When the current sample is the same as the previous sample, change~ outputs a sample of 0. When the current sample is less than the previous sample, change~ outputs a sample of -1.

Isn’t that simply HPZ1.kr(input).sign (or .ar)?

hjh

The simplification is appreciated, I did not realize HPZ1 already performed a difference between sample values. I haven’t noticed a thing after the change though.

I managed to fix the Fb1 version of my synth but the sound is quite harsher compared to the DynGen version. It had largely to do with the fact that only the 1st of the 2 feedbacked variables was used as a signal and not the 2nd, and the use of wrap2 on them instead of clip.

(
if (s.options.blockSize != 64) {
    s.options.blockSize = 64;
    s.quit.reboot;
}
)

(
u = SynthDef(\unstable, {
    | in1, in2, range2, in3, range3, in4, range4, clock_mod, bus_in, bus_out |

    // 1
    var oscn2_in1 = HPZ1.ar(
        LFPulse.ar(
                            1000 /  Ramp.kr(Ramp.kr(in1.linlin(0, 127, 0, 1000), 0.002).linlin(0, 1000, 1, 0.05), 0.02), width: 0.5
                        )
                    ).sign;
    // 8
    var cmod = Ramp.kr(Ramp.kr(clock_mod.linlin(0, 127, 0, 1), 0.002), 0.02);
    // 9
    var osc = In.ar(bus_in, 1);
    // 2 - 3
    var pow2 = (10 ** range2);
    var oscn2_in2 = Ramp.kr(Ramp.kr(in2.linlin(0, 127, 0, 1000), 0.002).linlin(0, 1000, -1 / pow2, 1 / pow2), 0.02) + (osc * cmod);
    // 4 - 5
    var pow3 = (10 ** range3);
    var oscn2_in3 = Ramp.kr(in3.linlin(0, 127, 0, 1000), 0.002).linlin(0, 1000, -1 / pow3, 1 / pow3);
    // 6 - 7
    var pow4 = (10 ** range4);
    var oscn2_in4 = Ramp.kr(in4.linlin(0, 127, 0, 1000), 0.002).linlin(0, 1000, -1 / pow4, 1 / pow4);

    var osc_sig = Fb1.ar({ |in, out|
        var md1, mc2, p_md1, p_mc2;
        var a1, a2, a3, a4;

        #a1, a2, a3, a4 = in[0];
        p_md1 = out[1][2].wrap2(1);
        p_mc2 = out[1][3].wrap2(1);

        md1 = if(a1 != 0, p_md1 + a2 - (0.1 * p_mc2), p_md1).sanitize;
        mc2 = (p_mc2 * (a4 + 1) + (((p_md1 * -1 * 0.01) + (p_md1 * 0.5)) * a3)).sanitize;

        [p_md1, p_mc2, md1, mc2]
    }, in: [oscn2_in1, K2A.ar(oscn2_in2), K2A.ar(oscn2_in3), K2A.ar(oscn2_in4)], outSize: 4);

    Out.ar(\bus_out.kr, osc_sig[0]!2);
});
)

(
~uns1 = NodeProxy.audio(s, 2);
~uns1.source = u;
Spec.add(\in1, ControlSpec( 0, 127, \lin, 1 ));
Spec.add(\in2, ControlSpec( 0, 127, \lin, 1 ));
Spec.add(\range2, ControlSpec( 0, 3, \lin, 1 ));
Spec.add(\in3, ControlSpec( 0, 127, \lin, 1 ));
Spec.add(\range3, ControlSpec( 0, 3, \lin, 1 ));
Spec.add(\in4, ControlSpec( 0, 127, \lin, 1 ));
Spec.add(\range4, ControlSpec( 0, 3, \lin, 1 ));
Spec.add(\clock_mod, ControlSpec( 0, 127, \lin, 1 ));
~uns1.gui;
~uns1.play;
s.scope;
)


(
~uns1.setn(
    \in1,  3,
    \in2, 40,
    \range2, 1,
    \in3, 64,
    \range3, 3,
    \in4, 63,
    \range4, 3,
    \clock_mod, 0);
)

One can better observe a sawtooth with the control rate version of the oscillator, although not quite audible. It seems in that sort of scenario, it would be desirable to be able to generate an audio rate signal from control rate NamedControls, like DynGen seems to allow.

(
if (s.options.blockSize != 64) {
    s.options.blockSize = 64;
    s.quit.reboot;
}
)

(
u = SynthDef(\unstable, {
    | in1, in2, range2, in3, range3, in4, range4, clock_mod, bus_in, bus_out |

    // 1
    var oscn2_in1 = HPZ1.kr(
        LFPulse.kr(
                            1000 /  Ramp.kr(Ramp.kr(in1.linlin(0, 127, 0, 1000), 0.002).linlin(0, 1000, 1, 0.05), 0.02), width: 0.5
                        )
                    ).sign;
    // 8
    var cmod = Ramp.kr(Ramp.kr(clock_mod.linlin(0, 127, 0, 1), 0.002), 0.02);
    // 9
    var osc = In.kr(bus_in, 1);
    // 2 - 3
    var pow2 = (10 ** range2);
    var oscn2_in2 = Ramp.kr(Ramp.kr(in2.linlin(0, 127, 0, 1000), 0.002).linlin(0, 1000, -1 / pow2, 1 / pow2), 0.02) + (osc * cmod);
    // 4 - 5
    var pow3 = (10 ** range3);
    var oscn2_in3 = Ramp.kr(in3.linlin(0, 127, 0, 1000), 0.002).linlin(0, 1000, -1 / pow3, 1 / pow3);
    // 6 - 7
    var pow4 = (10 ** range4);
    var oscn2_in4 = Ramp.kr(in4.linlin(0, 127, 0, 1000), 0.002).linlin(0, 1000, -1 / pow4, 1 / pow4);

    var osc_sig = Fb1.kr({ |in, out|
        var md1, mc2, p_md1, p_mc2;
        var a1, a2, a3, a4;

        #a1, a2, a3, a4 = in[0];
        p_md1 = out[1][2].wrap2(1);
        p_mc2 = out[1][3].wrap2(1);

        md1 = if(a1 != 0, p_md1 + a2 - (0.1 * p_mc2), p_md1).sanitize;
        mc2 = (p_mc2 * (a4 + 1) + (((p_md1 * -1 * 0.01) + (p_md1 * 0.5)) * a3)).sanitize;

        [p_md1, p_mc2, md1, mc2]
    }, in: [oscn2_in1, oscn2_in2, oscn2_in3, oscn2_in4], outSize: 4);

    Out.ar(\bus_out.kr, K2A.ar(osc_sig[0])!2);
});
)

(
~uns1 = NodeProxy.audio(s, 2);
~uns1.source = u;
Spec.add(\in1, ControlSpec( 0, 127, \lin, 1 ));
Spec.add(\in2, ControlSpec( 0, 127, \lin, 1 ));
Spec.add(\range2, ControlSpec( 0, 3, \lin, 1 ));
Spec.add(\in3, ControlSpec( 0, 127, \lin, 1 ));
Spec.add(\range3, ControlSpec( 0, 3, \lin, 1 ));
Spec.add(\in4, ControlSpec( 0, 127, \lin, 1 ));
Spec.add(\range4, ControlSpec( 0, 3, \lin, 1 ));
Spec.add(\clock_mod, ControlSpec( 0, 127, \lin, 1 ));
~uns1.gui;
~uns1.play;
s.scope;
)


(
~uns1.setn(
    \in1,  3,
    \in2, 40,
    \range2, 1,
    \in3, 64,
    \range3, 3,
    \in4, 63,
    \range4, 3,
    \clock_mod, 0);
)

(
~uns1.setn(
    \in1,  46,
    \in2, 127,
    \range2, 1,
    \in3, 64,
    \range3, 3,
    \in4, 63,
    \range4, 3,
    \clock_mod, 0);
)

Now experimenting with demand rate, I get a sawtooth, however the synth does not respond well to control rate NamedControls (only in2 and range2 works, changing the other parameters does not affect the sound), and trying to use more audio rate controls results in the server terminating.

(
u = SynthDef(\unstable, {
    | in1, in2, range2, in3, range3, in4, range4, clock_mod, bus_in, bus_out |

    // 1
    var oscn2_in1 = HPZ1.ar(
        LFPulse.ar(
                            1000 /  Ramp.kr(Ramp.kr(in1.linlin(0, 127, 0, 1000), 0.002).linlin(0, 1000, 1, 0.05), 0.02), width: 0.5
                        )
                    ).sign;
    // 8
    var cmod = Ramp.kr(Ramp.kr(clock_mod.linlin(0, 127, 0, 1), 0.002), 0.02);
    // 9
    var osc = In.ar(bus_in, 1);
    // 2 - 3
    var pow2 = (10 ** range2);
    var oscn2_in2 = Ramp.kr(Ramp.kr(in2.linlin(0, 127, 0, 1000), 0.002).linlin(0, 1000, -1 / pow2, 1 / pow2), 0.02) + (osc * cmod);
    // 4 - 5
    var pow3 = (10 ** range3);
    var oscn2_in3 = Ramp.kr(in3.linlin(0, 127, 0, 1000), 0.002).linlin(0, 1000, -1 / pow3, 1 / pow3);
    // 6 - 7
    var pow4 = (10 ** range4);
    var oscn2_in4 = Ramp.kr(in4.linlin(0, 127, 0, 1000), 0.002).linlin(0, 1000, -1 / pow4, 1 / pow4);
    var osc_sig;

    var b = LocalBuf(1);
    var c = LocalBuf(1);
    var read = { Dbufrd(b) };
    var write = { |x| Dbufwr(x, b) };
    var read2 = { Dbufrd(c) };
    var write2 = { |x| Dbufwr(x, c) };
    var rate = SampleRate.ir;
    var md1, mc2, pmd1, pmc2, a1, a2, a3, a4, y1, y2;
	pmd1 = read.wrap2(1.0);
	pmc2 = read2.wrap2(1.0);

    a1 = oscn2_in1;
    a2 = oscn2_in2;
    a3 = oscn2_in3;
    a4 = oscn2_in4;

    // will miserably crash the server
    //a2 = K2A.ar(oscn2_in2);
    //a3 = K2A.ar(oscn2_in3);
    //a4 = K2A.ar(oscn2_in4);

    md1 = if(a1 != 0, pmd1 + a2 - (0.1 * pmc2), pmd1);
    mc2 = (pmc2 * (a4 + 1) + (((pmd1 * -1 * 0.01) + (pmd1 * 0.5)) * a3));

	y1 = write.(md1);
	y2 = write.(mc2);
	osc_sig = Sanitize.ar(Duty.ar(1 / rate, 0, [y2, y1]));
    Out.ar(bus_out, osc_sig[1]);
});
)

(
Spec.add(\in1, ControlSpec( 0, 127, \lin, 1 ));
Spec.add(\in2, ControlSpec( 0, 127, \lin, 1 ));
Spec.add(\range2, ControlSpec( 0, 3, \lin, 1 ));
Spec.add(\in3, ControlSpec( 0, 127, \lin, 1 ));
Spec.add(\range3, ControlSpec( 0, 3, \lin, 1 ));
Spec.add(\in4, ControlSpec( 0, 127, \lin, 1 ));
Spec.add(\range4, ControlSpec( 0, 3, \lin, 1 ));
Spec.add(\clock_mod, ControlSpec( 0, 127, \lin, 1 ));
~uns1 = NodeProxy.audio(s, 2);
~uns1.source = u;
~uns1.setn(
    \in1,  3,
    \in2, 40,
    \range2, 1,
    \in3, 64,
    \range3, 3,
    \in4, 63,
    \range4, 3,
    \clock_mod, 0);
~uns1.gui;
~uns1.play;
s.scope;
)

Also, while the md1 variable can be sanitized, trying to sanitize mc2 results in the following error:

^^ ERROR: Message 'sanitize' not understood.
RECEIVER: a BinaryOpFunction