Fb1:
Filter:
miSCellaneous_lib/Classes (extension) | Libraries > miSCellaneous > Nonlinear

Fb1
ExtensionExtension

single sample feedback / feedforward pseudo ugen

Description

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 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.

Class Methods

.new

Creates a new Fb1 ar object.

Arguments:

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

Equivalent to *new.

.kr

Creates a new Fb1 kr object.

Arguments:

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 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

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 (Ex.2 ff.). Other than that you can use it to define higher order linear filters for which no classes exist.

 

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 the 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

 

Ex. 1b: OnePole

 

Ex. 1c: SOS

 

Ex. 1d: LTI

 

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: %

 

Ex. 2b: sin

 

Ex. 2c: *

 

Ex. 2d: /

 

Ex. 2e: **

 

Ex. 2f: Conditional feedback

Examples 3: Conventions and args

 

Ex. 3a: Multichannel feedback / feedforward

 

Ex. 3b: inInit / outInit

 

Ex. 3c: inDepth / outDepth

 

Ex. 3d: blockSize / blockFactor

 

Ex. 3e: func index

 

Ex. 4: Saving CPU

 

Ex. 5: Control rate