Fb1 single sample feedback / feedforward pseudo ugen
Part of: miSCellaneous
Inherits from: UGen
Fb1 provides an interface for single sample feedback and feedforward at audio and control rate, the defining relation with (formal) access to previous samples is passed as a Function, which might involve additional UGens. Fb1.ar works with arbitrary blockSizes and also allows to refer to samples earlier than one blockSize before. This includes linear filter definitions of arbitrary length with dynamic coefficients as well as all kinds of nonlinear calculations of feedback and feedforward data (FOS and SOS UGens cover the linear case with lengths 1 and 2, LTI the general linear case).
Fb1 at control rate exists since miSCellaneous v0.22, for compatibility reasons I left the convention that Fb1.new generates an ar UGen, so Fb1.new is now equivalent to Fb1.ar and Fb1.kr is possible in addition (See Ex.5). Fb1 is the base of an ordinary differential equation integrator framework for initial value problems, that also came with v0.22, see Fb1_ODE help for an introduction.
HISTORY AND CREDITS: There have been long discussions on single-sample feedback in SC. The most simple, but CPU-intense strategy is setting the server's blockSize to 1. Julian Rohrhuber gave a number of examples with Dbufrd / Dbufwr. SC's folder 'Examples' contains the files single_sample_feedback.scd and single_sample_feedback_02.scd. Special solutions are also possible with Delay1, Delay2 and other UGens. This particular implementation is based on Nathaniel Virgo's suggestion of iteratively writing to and reading from Buffers of blockSize – big credit for this! See also Nathaniel Virgo's Feedback quark for his feedback classes Fb and FbNode. Thanks also to James Harkins for his remarks on graph order. See Ex.1a for the basic feedback implementation principle. I implemented the ar feedforward option by temporary buffers for ar writing and kr reading, the feedback / feedforward relation can now be passed via a Function with 'in' and 'out' args. That way the syntax looks very similar to the common notations used for filter descriptions and also applies directly to the multichannel case. Most other options of Fb1 are for special multichannel handling and differentiated lookback definitions, which can help to save a lot of UGens.
WARNING: Be careful with amplitudes, feedback can become loud! It is highly recommended to take measures to avoid blowup, e.g. by limiting operators (tanh, softclip, distort) and/or using MasterFX from the JITLibExtensions quark. Also consider that short iteration cycles can produce loud high pitches, wrapping lopass filters is useful!
NOTE: The convenience of direct definition of the feedback / feedforward relation comes with the price of a large number of UGens involved. You might want to allow a higher number of UGens with the server option numWireBufs. You might also want to experiment with blockSizes smaller than 64 and larger than 1 (e.g. 8, 16 or 32). Check the graphOrderType arg, other values might cause considerable CPU saving and/or shortening of synthdef compile time.
See also: GFIS, Fb1_ODE, Fb1_ODEdef, Fb1_ODEintdef, Fb1, Fb1_MSD, Fb1_SD, Fb1_Lorenz, Fb1_Hopf, Fb1_HopfA, Fb1_HopfAFDC, Fb1_VanDerPol, Fb1_Duffing
Creation / Class Methods
*new (func, in, outSize = 0, inDepth = 1, outDepth = 2, inInit, outInit, blockSize = 64, blockFactor = 1, graphOrderType = 1, leakDC = true, leakCoef = 0.995)
Creates a new Fb1 ar object.
func - The Function to define the feedback / feedforward relation. The Function should take
the two arguments 'in' and 'out', both understood as nested multichannel signals,
additionally a block index is passed (Ex. 3e).
Each 'in' / 'out' item of the arrays represents current or previous samples,
for all points in time the samples are passed in specific array shapes,
which are determined by the shapes of in and outSize.
Allowed are pure signals (size = 0) and nested SequenceableCollections at maximum:
e.g. outSize can be 0, 3, or [0, 2, 5], accordingly in signals can be of sizes
0, i or [i1, ... , in] with i, ij >= 0.
Note that 'in' and 'out' only formally represent ar feedback and feedforward signals,
technically kr UGens (BufRd.kr) are passed, the ar signals are reconstructed at the end
by reading (arrays of) Buffers.
The Function should return the multichannel UGen to be referred to with 'out',
the shapes of the returned UGens and outSize must be the same.
Furthermore the meaning of 'in' and 'out' depends on the inDepth and outDepth arguments.
If an Integer is passed to them (default), the indices of 'in' resp. 'out' correspond to the
lookback indices:
E.g. out[1] refers to the last output sample(s) (of shape outSize), out[2] to the output sample(s) before
the last output sample(s) etc. This is compliant with the convention of writing out[i-1], out[i-2] etc.,
out[0] refers to out[i-blockSize].
For a multichannel 'in' / 'out' signal, depth can be differentiated, which saves UGens
in the case of "gaps" in the recursion:
E.g. for a three-channel out signal outDepth can look like [3, [7, 18], [2, 5, 6]].
Then out[1] is a three-channel signal, whereby out[1][0] corresponds to out[i-1] of the
first, out[1][1] to out[i-18] of the second and out[1][2] to out[i-5] of the third component.
If the size of inDepth / outDepth is smaller than outSize, wrapping is applied.
As a result double-bracketing can be used to define specific lookback indices
for all components of the multichannel signal:
E.g. if outDepth equals [[7, 18]] for a three-channel signal then out[0] means
the three-channel signal out[i-7] and out[1] means out[i-18].
See Ex. 3a for multichannel feedback / feedforward.
in - A single ar input signal or a SequenceableCollection of ar input signals to be referred to
with func (feedforward data). See Ex. 3a for multichannel feedback / feedforward.
outSize - Integer or SequenceableCollection thereof,
the size(s) defined by the UGen(s) returned by func.
It's the user's responsibilty to pass the correct size(s)!
Defaults to 0.
inDepth - Integer or SequenceableCollection of Integers or SequenceableCollections thereof,
this determines the behaviour of func (see there).
If an Integer is passed, it means the maximum storage size for feedforward data.
If a SequenceableCollection is passed, lookback indices for feedforward data
can be differentiated, its items can again be Integers or SequenceableCollections (see func).
Usually the inner SequenceableCollections should be ordered, but this is not compulsory.
Defaults to 1 (no lookback). See Ex. 3c.
outDepth - Integer or SequenceableCollection of Integers or SequenceableCollections thereof,
this determines the behaviour of func (see there).
If an Integer is passed, it means the maximum storage size for feedback data.
If a SequenceableCollection is passed, lookback indices for feedback data
can be differentiated, its items can again be Integers or SequenceableCollections (see func).
Usually the inner SequenceableCollections should be ordered, but this is not compulsory.
Defaults to 2 (look back to last sample at maximum). See Ex. 3c.
inInit - Number or SequenceableCollection, feedforward init data.
If a Number is passed, it means the previous init value for the calculation of the first sample(s),
if the size of in is larger than 1, this init value is taken for all components of the multichannel signal.
If a SequenceableCollection is passed, this differentiates the init values
for a multichannel signal 'in' used by func. Then the components must be Numbers
(again defining one init value) or SequenceableCollections, which
define a lookback collection: first Number is the previous value, second the value before and so on.
If the size of inInit is smaller than the size of in, wrapping is applied, that way a double-bracket array,
e.g. [[3, 0, 1]], defines the same init sequence for all components of a multichannel in. See Ex. 3b.
outInit - Number or SequenceableCollection, feedback init data.
If a Number is passed, it means the previous init value for the calculation of the first sample(s),
if outSize is larger than 1, this init value is taken for all components of the
multichannel signal 'out' used by func.
If a SequenceableCollection is passed, this differentiates the init values
for this multichannel signal. Then the components must be Numbers (again defining one init value)
or SequenceableCollections, which define a lookback collection:
first Number is the previous value, second the value before and so on.
If the size of outInit is smaller than outSize, wrapping is applied, that way a double-bracket array,
e.g. [[3, 0, 1]], defines the same init sequence for all components of the multichannel signal
'out' used by func. See Ex. 3b.
blockSize - Integer, this should be the server blockSize. It's the user's responsibility to pass
the correct number. However it might be interesting to experiment with other values.
Defaults to 64. See Ex. 3d.
blockFactor - Integer. For a value > 1 this allows for lookback indices larger than blockSize, up to
blockSize * blockFactor - 1. It's the user's responsibility to pass correct Integers in this case.
Defaults to 1. See Ex. 3d.
graphOrderType - 0, 1 or 2.
Determines if topological order of generated BufRd and BufWr instances
in the SynthDef graph is forced by additional UGens.
Type 0: forced graph order is turned off.
Type 1 (default): graph order is forced by summation and <!.
Type 2: graph order is forced by <! operators only.
Default 1 is recommended, but with CPU-intense SynthDefs it might be worth trying it with the value 0.
This saves a lot of UGens and in all my examples I didn't encounter cases with different results.
Type 2 can shorten the SynthDef compilation time for certain graphs with a large number of UGens,
which can be lengthy with type 1.
However, CPU usage doesn't directly correspond to the number of UGens.
leakDC - Boolean. Determines if a LeakDC is applied to the output.
Defaults to true.
leakCoef - Number, the leakDC coefficient. Defaults to 0.995.
*ar (func, in, outSize = 0, inDepth = 1, outDepth = 2, inInit, outInit, blockSize = 64, blockFactor = 1, graphOrderType = 1, leakDC = true, leakCoef = 0.995)
equivalent to *new
*kr (func, in, outSize = 0, inDepth = 1, outDepth = 2, inInit, outInit, graphOrderType = 1, leakDC = true, leakCoef = 0.995)
Creates a new Fb1 kr object.
func - The Function to define the feedback / feedforward relation. The Function should take
the two arguments 'in' and 'out', both understood as nested multichannel signals,
additionally a block index is passed (Ex. 3e).
Each 'in' / 'out' item of the arrays represents current or previous control samples,
for all points in time the control samples are passed in specific array shapes,
which are determined by the shapes of in and outSize.
Allowed are pure signals (size = 0) and nested SequenceableCollections at maximum:
e.g. outSize can be 0, 3, or [0, 2, 5], accordingly in signals can be of sizes
0, i or [i1, ... , in] with i, ij >= 0.
Note that 'in' and 'out' only formally represent feedback and feedforward signals,
technically kr UGens (BufRd.kr) are passed.
The Function should return the multichannel UGen to be referred to with 'out',
the shapes of the returned UGens and outSize must be the same.
Furthermore the meaning of 'in' and 'out' depends on the inDepth and outDepth arguments.
If an Integer is passed to them (default), the indices of 'in' resp. 'out' correspond to the
lookback indices:
E.g. out[1] refers to the last control output sample(s) (of shape outSize), out[2] to the control output sample(s) before
the last control output sample(s) etc. This is compliant with the convention of writing out[i-1], out[i-2] etc.
For a multichannel 'in' / 'out' signal, depth can be differentiated, which saves UGens
in the case of "gaps" in the recursion:
E.g. for a three-channel out signal outDepth can look like [3, [7, 18], [2, 5, 6]].
Then out[1] is a three-channel signal, whereby out[1][0] corresponds to out[i-1] of the
first, out[1][1] to out[i-18] of the second and out[1][2] to out[i-5] of the third component.
If the size of inDepth / outDepth is smaller than outSize, wrapping is applied.
As a result double-bracketing can be used to define specific lookback indices
for all components of the multichannel signal:
E.g. if outDepth equals [[7, 18]] for a three-channel signal then out[0] means
the three-channel signal out[i-7] and out[1] means out[i-18].
See Ex. 3a for multichannel feedback / feedforward.
in - A single kr input signal or a SequenceableCollection of kr input signals to be referred to
with func (feedforward data). See Ex. 3a for multichannel feedback / feedforward.
outSize - Integer or SequenceableCollection thereof,
the size(s) defined by the UGen(s) returned by func.
It's the user's responsibilty to pass the correct size(s)!
Defaults to 0.
inDepth - Integer or SequenceableCollection of Integers or SequenceableCollections thereof,
this determines the behaviour of func (see there).
If an Integer is passed, it means the maximum storage size for feedforward data.
If a SequenceableCollection is passed, lookback indices for feedforward data
can be differentiated, its items can again be Integers or SequenceableCollections (see func).
Usually the inner SequenceableCollections should be ordered, but this is not compulsory.
Defaults to 1 (no lookback). See Ex. 3c.
outDepth - Integer or SequenceableCollection of Integers or SequenceableCollections thereof,
this determines the behaviour of func (see there).
If an Integer is passed, it means the maximum storage size for feedback data.
If a SequenceableCollection is passed, lookback indices for feedback data
can be differentiated, its items can again be Integers or SequenceableCollections (see func).
Usually the inner SequenceableCollections should be ordered, but this is not compulsory.
Defaults to 2 (look back to last control sample at maximum). See Ex. 3c.
inInit - Number or SequenceableCollection, feedforward init data.
If a Number is passed, it means the previous init value for the calculation of the first control sample(s),
if the size of in is larger than 1, this init value is taken for all components of the multichannel signal.
If a SequenceableCollection is passed, this differentiates the init values
for a multichannel signal 'in' used by func. Then the components must be Numbers
(again defining one init value) or SequenceableCollections, which
define a lookback collection: first Number is the previous value, second the value before and so on.
If the size of inInit is smaller than the size of in, wrapping is applied, that way a double-bracket array,
e.g. [[3, 0, 1]], defines the same init sequence for all components of a multichannel in. See Ex. 3b.
outInit - Number or SequenceableCollection, feedback init data.
If a Number is passed, it means the previous init value for the calculation of the first control sample(s),
if outSize is larger than 1, this init value is taken for all components of the
multichannel signal 'out' used by func.
If a SequenceableCollection is passed, this differentiates the init values
for this multichannel signal. Then the components must be Numbers (again defining one init value)
or SequenceableCollections, which define a lookback collection:
first Number is the previous value, second the value before and so on.
If the size of outInit is smaller than outSize, wrapping is applied, that way a double-bracket array,
e.g. [[3, 0, 1]], defines the same init sequence for all components of the multichannel signal
'out' used by func. See Ex. 3b.
graphOrderType - 0, 1 or 2.
Determines if topological order of generated BufRd and BufWr instances
in the SynthDef graph is forced by additional UGens.
Type 0: forced graph order is turned off.
Type 1 (default): graph order is forced by summation and <!.
Type 2: graph order is forced by <! operators only.
Default 1 is recommended, but with CPU-intense SynthDefs it might be worth trying it with the value 0.
This saves a lot of UGens and in all my examples I didn't encounter cases with different results.
Type 2 can shorten the SynthDef compilation time for certain graphs with a large number of UGens,
which can be lengthy with type 1.
However, CPU usage doesn't directly correspond to the number of UGens.
leakDC - Boolean. Determines if a LeakDC is applied to the output.
Defaults to true.
leakCoef - Number, the leakDC coefficient. Defaults to 0.995.
Overview - what can / cannot be done ?
// What can be done:
// The feedback / feedforward relation is defined within func,
// it's important to note that this Function is applied in a very special way
// to build the feedback relation into the synthdef graph.
// Let n be the given blockSize, then
// 1.) ar / kr: func (only formally) takes over previous (multichannel) out samples for calculation of
// next (multichannel) out samples via its 'out' arg,
// technically BufRd.krs are passed to 'out' arg, in the ar case signals are reconstructed thereafter
// 2.) ar: func is applied n times to establish the iteration in the synthdef graph
// 3.) ar / kr: unary and binary operators are the basic tools for this calculation
// 4.) ar / kr: func can take over modulating kr UGens from outside via simple reference,
// for ar no linear interpolation in this case though, you might therefore consider (5)
// 5.) ar / kr: func can take over feedforward UGens with reference to their past data from outside via Fb1's and func's 'in' arg
// 6.) ar / kr: func can contain explicitely defined kr UGens.
// ar: note that for every UGen in func, n instances are built into the SynthDef graph!
// 7.)ar / kr: kr UGens in func can be applied to data passed via 'in' or 'out'
// 8.)ar: func's index argument can be used to specify the feedback / feedforward relation per block index
// What cannot / shouldn't be done (ar case considerations)
// Writing ar UGens in func that produce a time-varying signal itself
// (e.g. SinOsc.ar, in contrast to SinOsc.kr and operator UGens like '+', '*' etc.) -
// instead, if such ar UGens aren't applied to data from inside func,
// they can be passed via Fb1's and func's 'in' arg.
// It remains the case of such ar UGens that should process data that is provided by func
// (e.g. letting the fb out modulate a parameter of a VarSaw.ar).
// This is currently not possible and I don't have a clear picture if and how
// it would be possible at all or if it would make much sense.
Examples 1) Proof of concept
(
s = Server.local;
Server.default = s;
s.boot;
)
// Examples 1b-1d are just a comparison of standard filters vs. explicit definition with Fb1
// to show its functioning.
// Mostly there is no benefit in doing so in practice as standard filters UGens need less ressources.
// The real power of Fb1 lies in the potential to define nonlinear feedback and feedforward relations (2a-2f).
// Other than that you can use it to define higher order linear filters for which no classes exist.
// check blockSize before
(
if (s.options.blockSize != 64) {
s.options.blockSize = 64;
s.quit.reboot;
}
)
Ex.1a) Basic principle
// The original form of the following example is by Nathaniel Virgo
// and shows the underlying principle for feedback alone.
// Succesively Buffers are set with new values at kr,
// at the end buffers are read with an ar Phasor,
// thereby the order of UGens is crucial.
// As James Harkins remarked in the below thread,
// plugging the writers into the final reader forces it,
// it can be done with summing, but other operations than '+' are also possible.
// The example also works without this precautionary measure, at least on OSX, SC 3.9.3.
// The option graphOrderType allows to turn forced ordering off or to choose an
// alternative order-forcing operation, see Ex. 4.
// https://www.listarc.bham.ac.uk/lists/sc-users-2011/msg01337.html
// https://www.listarc.bham.ac.uk/lists/sc-users-2011/msg01363.html
(
x = {
var buf1 = LocalBuf(64);
var buf2 = LocalBuf(64);
var x1, x2;
var writer_1 = DC.ar(0);
var writer_2 = DC.ar(0);
SetBuf(buf1, [1], 63);
SetBuf(buf2, [0], 63);
x1 = BufRd.kr(1, buf1, 63);
x2 = BufRd.kr(1, buf2, 63);
64.do { |i|
x1 = x1 + (0.2 * x2);
x2 = tanh(x2 * 1.2 - (0.1 * x1));
writer_1 = writer_1 + BufWr.kr(x1, buf1, i);
writer_2 = writer_2 + BufWr.kr(x2, buf2, i);
};
// '<!' ensures that BufWrs are placed before the final reader
BufRd.ar(1, [buf1 <! writer_1, buf2 <! writer_2], Phasor.ar(0, 1, 0, 64)) * 0.1;
}.play;
)
x.release
// same written with Fb1
// per default LeakDC is applied, turn off here
(
y = {
Fb1({ |in, out|
var x1, x2; // define variables to adapt naming convention of above
// refer to last out samples
#x1, x2 = out[1];
x1 = x1 + (0.2 * x2);
x2 = tanh(x2 * 1.2 - (0.1 * x1));
[x1, x2]
}, outSize: 2, outInit: [1, 0], leakDC: false) * 0.1
}.play
)
y.release
// check if it's really the same, the difference should run silently (Fb1 without LeakDC)
(
z = {
var buf1 = LocalBuf(64);
var buf2 = LocalBuf(64);
var x1, x2;
var writer_1 = DC.ar(0);
var writer_2 = DC.ar(0);
SetBuf(buf1, [1], 63);
SetBuf(buf2, [0], 63);
x1 = BufRd.kr(1, buf1, 63);
x2 = BufRd.kr(1, buf2, 63);
64.do { |i|
x1 = x1 + (0.2 * x2);
x2 = tanh(x2 * 1.2 - (0.1 * x1));
writer_1 = writer_1 + BufWr.kr(x1, buf1, i);
writer_2 = writer_2 + BufWr.kr(x2, buf2, i);
};
BufRd.ar(1, [buf1 <! writer_1, buf2 <! writer_2], Phasor.ar(0, 1, 0, 64)) -
Fb1({ |in, out|
var x1, x2; // define variables to adapt naming convention of above
#x1, x2 = out[1];
x1 = x1 + (0.2 * x2);
x2 = tanh(x2 * 1.2 - (0.1 * x1));
[x1, x2]
}, outSize: 2, outInit: [1, 0], leakDC: false) * 0.1;
}.play;
)
z.release
Ex.1b) OnePole
// OnePole as lopass
s.freqscope;
x = { OnePole.ar(WhiteNoise.ar(0.3), 0.95) }.play;
x.release
// OnePole is implemented by out(i) = ((1 - abs(coef)) * in(i)) + (coef * out(i-1))
// so it can be written with Fb1
y = { Fb1({ |in, out| (in[0] * 0.05) + (out[1] * 0.95) }, WhiteNoise.ar(0.3), leakDC: false) }.play
y.release
// difference check: this falls silent (Fb1 without LeakDC)
(
z = {
var src = WhiteNoise.ar(0.5);
Fb1({ |in, out| (in[0] * 0.05) + (out[1] * 0.95) }, src, leakDC: false) - OnePole.ar(src, 0.95)
}.play
)
z.release
// stereo case: 'in' is passed a stereo noise,
// in addition we have to pass outSize: 2,
// then the Function can be written in the same way
// in[0] and out[1] now mean stereo signals
u = { Fb1({ |in, out| (in[0] * 0.05) + (out[1] * 0.95) }, WhiteNoise.ar(0.3!2), 2) }.play
u.release
// difference check with dynamic coefficient
// pass the ar signal via Fb1's 'in' arg
// difference nearly 0, even with fast modulation
(
v = {
var src = WhiteNoise.ar(0.5);
var mod = LFDNoise3.ar(50).range(0.7, 0.99);
(
Fb1({ |in, out|
var src, mod;
#src, mod = in[0];
(src * (1 - mod)) + (out[1] * mod)
}, [src, mod], leakDC: false) -
OnePole.ar(src, mod)
).poll
}.play
)
v.release
// Now check same with kr signal for coefficient,
// here we can directly take over the signal in func,
// difference is not zero though, because OnePole
// does linear interpolation with mod (Fb1 doesn't)!
(
w = {
var src = WhiteNoise.ar(0.5);
var mod = LFDNoise3.kr(50).range(0.7, 0.99);
(
Fb1({ |in, out| (in[0] * (1 - mod)) + (out[1] * mod) }, src, leakDC: false) -
OnePole.ar(src, mod)
).poll
}.play
)
w.release
// proof of concept with kr:
// If OnePole doesn't interpolate (as with Latch),
// signals are the same
(
q = {
var src = WhiteNoise.ar(0.5);
var mod = LFDNoise3.kr(50).range(0.7, 0.99);
(
Fb1({ |in, out| (in[0] * (1 - mod)) + (out[1] * mod) }, src, leakDC: false) -
OnePole.ar(src, Latch.ar(mod, TDuty.ar(ControlDur.ir)))
).poll
}.play
)
q.release
Ex.1c) SOS
// SOS as bandreject, note its convention:
// SOS.ar(in, a0, a1, a2, b1, b2, mul, add)
// out(i) = (a0 * in(i)) + (a1 * in(i-1)) + (a2 * in(i-2)) + (b1 * out(i-1)) + (b2 * out(i-2))
s.freqscope;
(
x = {
var a = [-0.6, 0.5, -0.7]; // feedforward coefficients a0, a1, a2
var b = [0.5, -0.1]; // feedback coefficients b1, b2
var src = Saw.ar(200, 0.1);
SOS.ar(src, *(a ++ b))
}.play
)
x.release
// written with Fb1
(
y = {
var a = [-0.6, 0.5, -0.7];
var b = [0.5, -0.1];
var src = Saw.ar(200, 0.1);
// out[0] (= out[i-blockSize]) is passed formally to allow taking over the usual index convention
// here we drop it as b1 = b[0] and b2 = b[1]
// as we look back two samples for feedforward and feedback we need to pass depths 3
Fb1({ |in, out| (in * a).sum + (out.drop(1) * b).sum }, src, 0, 3, 3, leakDC: false)
}.play
)
y.release
// difference check: this falls silent
(
z = {
var a = [-0.6, 0.5, -0.7];
var b = [0.5, -0.1];
var src = Saw.ar(200, 0.1);
(
Fb1({ |in, out| (in * a).sum + (out.drop(1) * b).sum }, src, 0, 3, 3, leakDC: false) -
SOS.ar(src, *(a ++ b)) // '*' splits the array into single items needed as UGen args
).poll
}.play
)
z.release
Ex.1d) LTI
// You need to have the SC-3 plugins installed in order to use LTI,
// which is part of Nick Collins' SLUGens.
// Example with coefficients from Nick Collins' helpfile,
// here 'a' is used for feedback coefficients.
// LTI needs data in buffers
(
a = [0.02, -0.01]; // feedback coefficients
b = [1, 0.7, 0, 0, 0, 0, -0.8, 0, 0, 0, 0, 0.9] ++
[0, 0, 0, -0.5, 0, 0, 0, 0, 0, 0, 0.25, 0.1, 0.25]; // feedforward coefficients
c = Buffer.sendCollection(s, a, 1);
d = Buffer.sendCollection(s, b, 1);
)
x = { LTI.ar(Saw.ar(50, 0.1), c.bufnum, d.bufnum) }.play
x.release
// Because of the large inDepth (25 !) a straight takeover of data is very CPU-demanding with Fb1
(
y = {
var src = Saw.ar(50, 0.1);
Fb1({ |in, out| (in * b).sum + (out.drop(1) * a).sum }, src, 0, b.size, 3, leakDC: false)
}.play
)
y.release
// This is an example, where passing lookback indices as depth arg helps a lot (more than 1000 UGens less!)
(
z = {
var src = Saw.ar(50, 0.1);
var ff = b.reject(_ == 0); // [1, 0.7, -0.8, 0.9, -0.5, 0.25, 0.1, 0.25]
// for SC < 3.7 you can write (0..b.size-1).select { |i| b[i] != 0 }
var indices = b.selectIndices(_ != 0); // [0, 1, 6, 11, 15, 22, 23, 24]
Fb1({ |in, out| (in * ff).sum + (out.drop(1) * a).sum }, src, 0, [indices], 3, leakDC: false)
}.play
)
z.release
// difference check, run silently
(
u = {
var src = Saw.ar(50, 0.1);
var ff = [1, 0.7, -0.8, 0.9, -0.5, 0.25, 0.1, 0.25];
var indices = [0, 1, 6, 11, 15, 22, 23, 24];
(Fb1({ |in, out| (in * ff).sum + (out.drop(1) * a).sum }, src, 0, [indices], 3, leakDC: false) -
LTI.ar(src, c.bufnum, d.bufnum)).poll
}.play
)
u.release
// The filter alternatives with Fb1 mainly make sense in the higher order case when coefficients should be modulated
Examples 2) Nonlinear feedback operations
// This is an interesting field of exploration as it cannot easily be done with SC otherwise.
// Here the time-varying potential of unary and binary operators comes into play,
// normally they are applied to time-varying signals, but here their iteration on a
// single-sample base is itself an essential part of producing the variation in time.
// Good candidates are already simple operators and their combinations, also with + and -,
// e.g. *, /, **, %, trigonometric operators etc.
Ex.2a) %
s.scope
// start fb fx Synth
(
~bus = Bus.audio(s, 2);
y = {
var inSig = In.ar(~bus, 2);
var lfo = SinOsc.ar(0.1).linexp(-1, 1, 50, 500);
LPF.ar(
Fb1({ |in, out|
(out[2] * 0.7) + // factors > 1 blow up !
// only the next line is limited with softclip
// in[0][0] is the current stereo signal from the bus
// in[1][0] is the previous
// in[0][1] is the current mono lfo, passed with the 'in' arg
// func returns a stereo signal, so outSize must be passed 2
// inDepth [2, 1] because we use inSig[0], inSig[1] and lfo[0]
// outDepth 3 because we use out[2]
// note that it would be cheaper to use out[0] and outDepth: [[2]],
// but a bit more difficult to read
((in[0][0] - in[1][0]) % 0.01 * in[0][1]).softclip
}, [inSig, lfo], 2, [2, 1], 3
), 15000) * 0.1
}.play
)
// start source
x = { Out.ar(~bus, SinOsc.ar([60, 60.1]) * EnvGate.new) }.play
// stop source and start new one
x.release
x = { Out.ar(~bus, Saw.ar(SinOsc.ar(0.07).linlin(-1, 1, [100, 100.01], 100.2) * EnvGate.new)) }.play
x.release
(
y.free;
~bus.free;
)
Ex.2b) sin
// sin can work as a limiter as well as a nonlinear dynamics engine,
// here it causes a wavefolding-like effect
s.scope
// start fb fx Synth
(
~bus = Bus.audio(s, 1);
y = {
var inSig = In.ar(~bus);
var lfo = SinOsc.ar(0.1, -pi/2).linexp(-1, 1, [5, 10], 100) * 100;
LPF.ar(
Fb1({ |in, out|
// here out[0] refers to out[i-2] because of outDepth: [[2]]
(out[0] * 0.7) + // factors > 1 blow up !
// here in[0][0] and in[1][0] are current and previous mono inSig,
// but in[0][1] is stereo, so again a stereo signal is returned by func,
// which must be indicated with the outSize arg
((in[0][0] - in[1][0]) * in[0][1]).sin
}, [inSig, lfo], 2, [2, 1], [[2]]
) * 0.05, 15000)
}.play
)
// start source
x = { Out.ar(~bus, SinOsc.ar(60) * EnvGate.new) }.play;
x.release;
// stop source and start new one
x = { Out.ar(~bus, Saw.ar(SinOsc.ar(0.05).linlin(-1, 1, 100, 100.02) * EnvGate.new)) }.play
x.release
(
y.free;
~bus.free;
)
Ex.2c) *
// rather irrational concatenation of simple operations
// depth changes cause frequency changes
s.scope
// start fb fx Synth
(
~bus = Bus.audio(s, 1);
y = {
var inSig = In.ar(~bus);
var lfo = { LFDNoise3.ar(0.07).linexp(-1, 1, 1, 5) } ! 2;
var i = Demand.kr(Dust.kr(0.5), 0, Dxrand((0..2), inf));
var sig = Fb1({ |in, out|
(
in[0][1] * (
in[0][0] * 0.12 + (
// changes between out[i-23], out[i-41] and out[i-60] cause frequency changes,
// Select depends on a signal from outside, not previous samples as in Ex. 2f
(in[1][0].squared - Select.kr(i, out).squared).sqrt
)
)
).tanh
}, [inSig, lfo], 2, [2, 1], [[23, 41, 60]]) * 0.1;
// add frequency modulation by delay modulation
// lopass filtering with lag
DelayC.ar(sig, 0.2, LFDNoise3.ar(1).range(0.01, 0.1)).lag(0.0005)
}.play
)
// start source
x = { Out.ar(~bus, LFTri.ar(LFDNoise0.ar(5).exprange(1, 100)) * EnvGate.new) }.play;
x.release
(
y.free;
~bus.free;
)
Ex.2d) /
// With divisions we must avoid division by zero resp. blowup, here it's max in the divisor.
// The example also establishes a cross-feedback of the two channels by using reverse.
s.scope
// start fb fx Synth
(
~bus = Bus.audio(s, 2);
y = {
var inSig = In.ar(~bus, 2);
var lfo = LFDNoise3.ar(1).linexp(-1, 1, 0.2, 10);
LPF.ar(
Fb1({ |in, out|
(in[1][0] * in[0][1] / max(0.001, (in[1][0] - out[1].reverse).abs)).tanh
}, [inSig, lfo], 2, [2, 1], 2
), 15000) * 0.1
}.play
)
// start source
x = { Out.ar(~bus, SinOsc.ar(LFDNoise3.ar(0.1!2).range(100, 101)) * EnvGate.new) }.play
x.release
(
y.free;
~bus.free;
)
Ex.2e) **
// exponentiation can also be interesting
// here an area of instability is crossed by a stereo lfo
s.scope
// start fb fx Synth
(
~bus = Bus.audio(s, 1);
y = {
var inSig = In.ar(~bus);
var lfo = { LFDNoise3.ar(0.5).linexp(-1, 1, 0.1, 150) } ! 2;
Fb1({ |in, out|
in[1][0] * 0.07 +
// out[0] refers to out[i-2] because of outDepth: [[2]]
(2 ** (in[1][0] - out[0] * in[0][1]).abs).tanh
}, [inSig, lfo], 2, [2, 1], [[2]]) *
// avoid bump at start
EnvGen.ar(Env.asr(2))
}.play
)
// start source
x = { Out.ar(~bus, LFTri.ar(60) * EnvGate.new) }.play;
x.release
(
y.free;
~bus.free;
)
Ex.2f) Conditional feedback
// defining the next sample depending on some characteristics of the previous one(s)
// This can be done with the if UGen and Select.
// 'if' doesn't support multichannel expansion, so take Select here
s.scope
// noisy texture with beeps
(
x = {
var src = LFDNoise3.ar(1, 0.1);
// ar modulators to be passed (avoid annoying steady tone caused by kr)
var mod1 = LFDNoise3.ar(1).range(0.01, 0.2);
// already slight difference results in quite strong stereo decorrelation
var mod2 = LFDNoise3.ar(1).range([0.0001, 0.0002], 0.0049);
Fb1({ |in, out|
// give same names as above for better readability
var src = in[0][0];
var mod1 = in[0][1];
var mod2 = in[0][2];
softclip(
Select.kr(
// as mod2 is stereo we get stereo expansion
// and in turn different selections
// outDepth = [[1, 6]]
// so out[0] refers to out[i-1], out[1] to out[i-6]
out[0] % 0.005 < mod2,
[out[1].neg * mod1, out[0] * 0.1]
) + src + out[0]
)
// lopass filtering with lag
}, [src, mod1, mod2], 2, 1, [[1, 6]]).lag(0.001) * 0.5
}.play
)
x.release
Examples 3) Conventions and args
Ex.3a) Multichannel feedback / feedforward
// in and out can be multichannel signals of arbitrary size or collections thereof,
// however arbitrary nesting is not supported,
// outSize arg has to be passed explicitely,
// size of in arg is taken over automatically.
// also mind the difference between size 0 and 1:
// with outSize of 1 or [0] Fb1 returns an array
// here out is of size 3, in of sizes [3, 0]
(
x = {
var inSig = SinOsc.ar(LFDNoise3.ar(0.01 ! 3).range(100, 101)); // 3 channel in signal
var lfo = LFDNoise3.ar(0.1).linexp(-1, 1, 0.2, 10);
var sig;
sig = LPF.ar(
Fb1({ |in, out|
// in[0][0] and in[1][0] represent current and previous 3 channel samples from inSig
// in[0][1] represents current sample from lfo
// rotate causes cross-feedback of 3 channels
// with reverse only first and last would cross
(in[1][0] * in[0][1] / max(0.001, (in[1][0] - out[1].rotate(1)).abs)).tanh
// outSize 3 has to be passed
}, [inSig, lfo], 3, [2, 1], 2
), 15000) * 0.1;
Splay.ar(sig)
}.play
)
x.release
// again out is of size 3, in of sizes [3, 3]
(
x = {
var inSig = SinOsc.ar(LFDNoise3.ar(0.01 ! 3).range(100, 101)); // 3 channel in signal
var mod = SinOsc.ar([1, 2.001, 3.999] * 120).linexp(-1, 1, 0.2, 10);
var sig;
sig = LPF.ar(
Fb1({ |in, out|
// in[0][0] and in[1][0] represent current and previous 3 channel samples from inSig
// in[0][1] represents current 3 samples from mod
// rotate causes cross-feedback of 3 channels
// with reverse only first and last would cross
(in[1][0] * in[0][1] / max(0.001, (in[1][0] - out[1].rotate(1)).abs)).tanh
// outSize has to be passed
}, [inSig, mod], 3, [2, 1], 2
), 15000) * 0.05;
Splay.ar(sig)
}.play
)
x.release
// it's also possible to let func return an array of (multichannel) signals,
// outSize must be set accordingly, here [2, 0],
// whereby the mono within the array is a "helper feedback"
(
x = {
var inSig = SinOsc.ar(LFDNoise3.ar(1.5 ! 2).exprange(50, 100));
var lfo = LFDNoise3.ar(5).linexp(-1, 1, 0.5, 100);
var sig;
sig = LPF.ar(
Fb1({ |in, out|
// in[1][0] represents previous 2 channel samples from inSig
// in[0][1] represents current sample from lfo
// reverse causes cross-feedback of 2 main channels
// main feedback crosses with helper feedback
[
// for main feedback use helper feedback
(out[1][1] / max(0.001, (in[1][0] - out[1][0].reverse).abs)).tanh,
// for helper feedback use first channel of main feedback
(out[1][0][0] + 0.1 / max(0.01, ((in[0][1].abs)))).tanh
]
// outSize [2, 0] has to be passed
}, [inSig, lfo], [2, 0], [2, 1], 2
), 12000) * 0.2;
// return main feedback
sig[0]
}.play
)
x.release
// here 2 x stereo, outSize == [2, 2]
(
x = {
// two stereo sources
var in_1 = SinOsc.ar(LFDNoise3.ar(0.1 ! 2).range(100, 101));
var in_2 = SinOsc.ar(LFDNoise3.ar(0.1 ! 2).range(150, 151));
var lfo = LFDNoise3.ar(0.1).linexp(-1, 1, 0.2, 10);
var sig;
sig = LPF.ar(
Fb1({ |in, out|
// rename for better readability
// previous ins are stereo
var prevIn_1 = in[1][0];
var prevIn_2 = in[1][1];
// mono lfo
var lfo = in[0][2];
// out is 2 x 2 (see below)
var prevOut_1 = out[1][0]; // stereo
var prevOut_2 = out[1][1]; // stereo
// we return an array of two stereo signals
[
(prevIn_1 * lfo / max(0.001, (prevIn_1 - prevOut_1.reverse).abs)),
(prevIn_2 * lfo / max(0.001, (prevIn_2 - prevOut_2.reverse).abs))
].tanh
// outSize [2, 2] has to be passed
}, [in_1, in_2, lfo], [2, 2], 2, 2
), 15000) * 0.05;
sig[0] + sig[1]; // mix together
// sig[0];
// sig[1];
}.play
)
x.release
// variant: cross feedback within first stereo out (a) plus
// cross feedback the stereo signals with each other (b)
// at the end take only first stereo out
// because of (b) the 150 Hz of in_2 are contained in the resulting signal
(
x = {
var in_1 = SinOsc.ar(LFDNoise3.ar(0.1 ! 2).range(100, 101));
var in_2 = SinOsc.ar(LFDNoise3.ar(0.1 ! 2).range(150, 151));
var lfo = LFDNoise3.ar(0.1).linexp(-1, 1, 0.2, 10);
var sig;
sig = LPF.ar(
Fb1({ |in, out|
// rename for better readability
// previous ins are stereo
var prevIn_1 = in[1][0];
var prevIn_2 = in[1][1];
// mono lfo
var lfo = in[0][2];
// out is 2 x 2 (see below)
var prevOut_1 = out[1][0]; // stereo
var prevOut_2 = out[1][1]; // stereo
// we return an array of two stereo signals
[
(prevIn_1 * lfo / max(0.001, (prevIn_1 - prevOut_2.reverse).abs)),
(prevIn_2 * lfo / max(0.001, (prevIn_2 - prevOut_1).abs))
].tanh
// outSize has to be passed
}, [in_1, in_2, lfo], [2, 2], 2, 2
), 15000) * 0.05;
sig[0]
}.play
)
x.release
Ex.3b) inInit / outInit
// linear congruential generator
// this is not a strict linear congruential generator
// as the server doesn't know integers, it's done with floats,
// all is blurred by floating point inaccuracy
// however interesting results can be obtained
// different start values can produce different orbits
// WARNING: can produce loud high pitches with certain init values and factors
// as a result of short iteration cycles, take LPF !
// same init value for both channels
(
x = {
var sig = Fb1({ |in, out|
out[1] * 5.239 % 1
},
outSize: 2,
outInit: 1 // [1] is equivalent
).tanh * 0.2;
LPF.ar(sig, 2000)
}.play
)
x.release
// other iteration sequence by different init value
(
x = {
var sig = Fb1({ |in, out|
out[1] * 5.239 % 1
},
outSize: 2,
outInit: 2
).tanh * 0.2;
LPF.ar(sig, 2000)
}.play
)
x.release
// defining different init values per channel
(
x = {
var sig = Fb1({ |in, out|
out[1] * 5.239 % 1
},
outSize: 2,
outInit: [1, 2]
).tanh * 0.2;
LPF.ar(sig, 2000)
}.play
)
x.release
// mono, two init values for one channel (previous and previous of previous)
// this needs double brackets for outInit
(
x = {
var sig = Fb1({ |in, out|
out[1] * 2 + (out[2] * 3) % 1.01
},
outSize: 1,
outInit: [[2, 6]],
outDepth: 3 // needed as we look back for 2 values
).tanh * 0.2;
LPF.ar(sig, 2000)
}.play
)
x.release
// stereo, different arrays of init values per channel
(
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
)
x.release
// inInit values can be defined in the same way as outInit
// want a trigger at start in connection with Dust
(
x = {
var trig = Dust.ar(0.3);
var src = SinOsc.ar(1000);
var sig = Fb1({ |in, out|
var tr = in[1][0];
var mod = in[0][1];
(out[1] + tr * (mod * 0.02 + 0.999999))
},
in: [trig, src],
inDepth: 2,
// here both in buffers get init values, only the first is relevant
inInit: 1, // [1, 0] doesn't make a difference
).lag(0.005).tanh * 0.5;
sig
}.play
)
x.release
// inInit and outInit cannot be differentiated for a multichannel component of a multichannel in / out.
// If you want to do such, you'd have to split the inner
// multichannel component and differentiate the outer one.
Ex.3c) inDepth / outDepth
// Normally, and like in most previous examples,
// out[j] and in[j] in func refer to samples out[i-j] and in[i-j],
// in[0] to the current input samples.
// The lookback size can be determined by passing Integers to inDepth / outDepth, e.g.
// with inDepth = 2 you can refer to in[0] and in[i-1]
// with outDepth = 3 you can refer to out[i-1] and out[i-2],
// out[0] refers to out[i-blockSize].
// When refering not to previous but to earlier samples,
// passing specified inDepth / outDepth indices is saving UGens.
// Here in[0] refers to in[0], the current sample(s), and
// in[1] refers to in[i-56].
(
x = {
var src = SinOsc.ar(500 * LFDNoise3.ar(5));
var sig = Fb1({ |in, out|
(out[1] / max(0.01, (in[1] - in[0]))).tanh
},
in: src,
inDepth: [[0, 56]],
outInit: 1
) ! 2 * 0.1 ;
sig
}.play
)
x.release
// looking back less far changes the sound colour
(
x = {
var src = SinOsc.ar(500 * LFDNoise3.ar(5));
var sig = Fb1({ |in, out|
(out[1] / max(0.01, (in[1] - in[0]))).tanh
},
in: src,
inDepth: [[0, 29]],
outInit: 1
) ! 2 * 0.1;
sig
}.play
)
x.release
// differentiate inDepth per channel
(
x = {
// stereo in
var src = SinOsc.ar(500 * LFDNoise3.ar(5!2));
var sig = Fb1({ |in, out|
(out[1] / max(0.01, (in[1] - in[0]))).tanh
},
outSize: 2,
in: src,
inDepth: [[0, 29], [0, 56]],
outInit: 1
) * [0.2, 0.1];
sig
}.play
)
x.release
// inDepth and outDepth cannot be differentiated for a multichannel component of a multichannel in / out.
// If you want to do such, you'd have to split the inner
// multichannel component and differentiate the outer one.
Ex.3d) blockSize / blockFactor
// Normally Fb1's blockSize should equal the server's current blockSize,
// which can be set as a server option, per default it's 64.
// If you are using a different blockSize, you can either
// reset it for the examples in this helpfile ...
(
if (s.options.blockSize != 64) {
s.options.blockSize = 64;
s.quit.reboot;
}
)
// ... or run the examples with passing a different blockSize, e.g. with:
Fb1(..., blockSize: s.options.blockSize)
// You can however try to creativily use a "wrong" blockSize and play with artefacts,
// variant of Ex. 3c
(
x = {
// stereo in
var src = SinOsc.ar(300 * LFDNoise3.ar(1!2));
var sig = Fb1({ |in, out|
(out[1] / max(0.01, (in[1] - in[0]))).tanh
},
outSize: 2,
in: src,
inDepth: [[0, 15], [0, 19]],
outInit: 1,
blockSize: 33
) * 0.2;
LPF.ar(sig, 3000)
}.play
)
x.release
// variant of Ex. 3c
// suppose a blockSize of 64, to look back to in[i-150] set blockFactor to 3.
(
x = {
// stereo in
var src = SinOsc.ar(500 * LFDNoise3.ar([0.3, 7]));
var sig = Fb1({ |in, out|
(out[1] / max(0.001, (in[1] - in[0]))).distort
},
outSize: 2,
in: src,
inDepth: [[0, 150], [0, 29]],
outInit: 1,
blockFactor: 3
) * [0.07, 0.15];
sig
}.play
)
x.release
Ex.3e) func index
// func can take an index as third arg, it runs from 0 to blockSize - 1
// this can be used to define the feedback relation depending on it
// note that inDepth is set to [2, 1] as we look back to inSig[i-1] (in[1][0]),
// but not to lfo[i-1] (in[0][1] == lfo[0]), this saves 128 UGens !
(
x = {
var inSig = SinOsc.ar([50, 50.1]);
var lfo = SinOsc.ar(LFDNoise3.ar(0.1).range(0, 500)).range(0, [100, 105]);
LPF.ar(
Fb1({ |in, out, i|
(
// establish alternating feedback relations in the synthdef graph
i.odd.if {
in[0][0] * in[0][1] + out[1]
}{
(in[0][0] - in[1][0]) * out[1]
}
).tanh
}, [inSig, lfo], 2, [2, 1], 2
) * 0.1, 12000)
}.play
)
x.release
Ex.4) Saving CPU
// UGens written in func are generated as often as blockSize.
// Therefore, if possible, references to kr UGens outside save resources.
// In addition look for hidden unnecessary operations, which can add hundreds of UGens
// 2440 UGens (with blockSize == 64)
// deliberately bad, deterministic lfo is generated blockSize times
(
x = {
var sig, src;
src = SinOsc.ar(90 * LFDNoise3.ar(0.3!2).range(0.98, 1.02)) * SinOsc.ar(45.25);
sig = Fb1({ |in, out|
var a = in[0];
var b = out[1];
var lfo = SinOsc.kr(SinOsc.kr(0.1).range(0.03, 1)).range(0.001, 0.01);
softclip((a * a * a) + (a * a) + a + (a * a * b) / max(lfo, a + b));
}, src, 2) * 0.1;
LPF.ar(sig, 3000)
}.play
)
x.release
// 2188 UGens as (blockSize - 1) * 4 = 63 * 4 = 252 UGens are saved
(
x = {
var sig, src, lfo;
src = SinOsc.ar(90 * LFDNoise3.ar(0.3!2).range(0.98, 1.02)) * SinOsc.ar(45.25);
lfo = SinOsc.kr(SinOsc.kr(0.1).range(0.03, 1)).range(0.001, 0.01);
sig = Fb1({ |in, out|
var a = in[0];
var b = out[1];
softclip((a * a * a) + (a * a) + a + (a * a * b) / max(lfo, a + b));
}, src, 2) * 0.1;
LPF.ar(sig, 3000)
}.play
)
x.release
// still bad though when looking at the the algebraic identity
// a^3 + a^2 + a + (a * a * b) = ((a + b) * a + a) * a + a
// these are 8 vs 5 operations, so with stereo we can save further
// ((8 - 5) * 2) * 64 = 384 UGens
// 1804 UGens
(
x = {
var sig, src, lfo;
src = SinOsc.ar(90 * LFDNoise3.ar(0.3!2).range(0.98, 1.02)) * SinOsc.ar(45.25);
lfo = SinOsc.kr(SinOsc.kr(0.1).range(0.03, 1)).range(0.001, 0.01);
sig = Fb1({ |in, out|
var a = in[0];
var b = out[1];
// with SC's L/R-precendence we can write without brackets
softclip(a + b * a + a * a + a / max(lfo, a + b));
}, src, 2) * 0.1;
LPF.ar(sig, 3000)
}.play
)
x.release
// without forcing the topological order of BufRds and BufWrs further UGens are saved,
// this might or might not be the same result, might be distorted in worst case,
// however in all my tests I didn't encounter a single example where it was different
// 1713 UGens
(
x = {
var sig, src, lfo;
src = SinOsc.ar(90 * LFDNoise3.ar(0.3!2).range(0.98, 1.02)) * SinOsc.ar(45.25);
lfo = SinOsc.kr(SinOsc.kr(0.1).range(0.03, 1)).range(0.001, 0.01);
sig = Fb1({ |in, out|
var a = in[0];
var b = out[1];
// with SC's L/R-precendence we can write without brackets
softclip(a + b * a + a * a + a / max(lfo, a + b));
}, src, 2, graphOrderType: 0) * 0.1;
LPF.ar(sig, 3000)
}.play
)
x.release
// check if it's the same - run silently
(
x = {
var sig, src, lfo;
src = SinOsc.ar(90 * LFDNoise3.ar(0.3!2).range(0.98, 1.02)) * SinOsc.ar(45.25);
lfo = SinOsc.kr(SinOsc.kr(0.1).range(0.03, 1)).range(0.001, 0.01);
sig = Fb1({ |in, out|
var a = in[0];
var b = out[1];
// with SC's L/R-precendence we can write without brackets
softclip(a + b * a + a * a + a / max(lfo, a + b));
}, src, 2, graphOrderType: 0) -
Fb1({ |in, out|
var a = in[0];
var b = out[1];
softclip(a + b * a + a * a + a / max(lfo, a + b));
}, src, 2) * 0.1;
LPF.ar(sig, 3000)
}.play
)
x.release
// see also Ex.5 for saving CPU with kr
Ex.5) Control rate
// Ex.4 with kr and K2A
(
x = {
var sig, src, lfo;
src = SinOsc.kr(90 * LFDNoise3.kr(0.3!2).range(0.98, 1.02)) * SinOsc.kr(45.25);
lfo = SinOsc.kr(SinOsc.kr(0.1).range(0.03, 1)).range(0.001, 0.01);
sig = Fb1.kr({ |in, out|
var a = in[0];
var b = out[1];
// with SC's L/R-precendence we can write without brackets
softclip(a + b * a + a * a + a / max(lfo, a + b));
}, src, 2, graphOrderType: 0) * 0.1;
LPF.ar(K2A.ar(sig), 3000)
}.play
)
x.release
// FM with same signal
(
x = {
var sig, src, lfo;
src = SinOsc.kr(90 * LFDNoise3.kr(0.3!2).range(0.98, 1.02)) * SinOsc.kr(45.25);
lfo = SinOsc.kr(SinOsc.kr(0.1).range(0.03, 1)).range(0.001, 0.01);
sig = Fb1.kr({ |in, out|
var a = in[0];
var b = out[1];
// with SC's L/R-precendence we can write without brackets
softclip(a + b * a + a * a + a / max(lfo, a + b));
}, src, 2, graphOrderType: 0) * 0.1;
SinOsc.ar(2000 * sig, 0, 0.1)
}.play
)
x.release