PbindFx event pattern for effect handling on per-event base 


Part of: miSCellaneous


Inherits from: Pchain



PbindFx works like a normal Pbind of event type 'note' in most regards, but with the additional option to define a number of effects. Their order and parameters can also be defined with patterns which allows a great flexibility: for each event an arbitrary multichannel effect graph can be applied, mixing sequential and parallel arrangement ad libitum. This requires a relatively high amount of resource management: bus allocation, routing and node ordering as well as delayed cleanup have to be done for each event. All necessary bookkeeping is done automatically, for some critical parameters though it's the user's responsibility to pass meaningful values (e.g. cleanupDelay for reverb has to be defined sufficiently high, otherwise reverb synth and audio bus might be freed before reverb has ended). There is always a tradeoff between flexibility and processing effort, if you won't change fx parameters on a per-event base or you won't reorder your effect arrangement, then you might prefer playing effects from and to predefined buses and control them otherwise, e.g. per LFOs (additionally possible also with PbindFx) or dedicated setting streams, see PmonoPar and PpolyPar. However with the strategy of effects bound to buses you have the same effect arrangements and parameters concerning all source signals sent to the same bus, more variation with such setups needs more (pre-)definition of buses, whereas with PbindFx overlapping events can be processed with different effect arrangements and parameters with no explicit effort.  


WARNING:

As bus allocation is done dynamically per event, there is a circumvented, but still potential danger of creating feedback loops. To prevent this, additional "zero synths" are started with bus-reading fx synths, playing a zero signal with ReplaceOut to the buses in question, they are placed before those fx / source synth(s), which will play there too. For all test examples, even with deliberately bad values, zero synths turned out to be an effective way to block unwanted input signals and feedback, as they last as long as fx / source synths (they even overlap them a bit). However, with extraordinary parameter values for timing, improperly defined fx / source synths, sloppy audio bus mapping and / or parallel actions that affect resource management globally, feedback, as in any situation of heavy bus repatching, can not be totally excluded. Be aware of that, avoid high levels and be careful with headphones !




Creation / Class Methods


*new (pbindData ... fxData)

Creates a new PbindFx object.

pbindData - SequenceableCollection of Pbind's key/value pairs or event pattern.

Passing a list saves explicitely typing Pbind, but passing an event pattern is more flexible,

as it allows replacement (Ex.7), event pattern filtering (Ex.3a) and similar operations.


specific keys:

\fxOrder - For each event it can be given in three ways, for single effects and

sequential ordering it may be 

(a) an Integer, or 

(b) a SequenceableCollection of Integers, 

indicating the effect order. Effect counting starts with 1, 0 means no effect (default).

E.g. if three effects are passed to fxData, [1], [2], [3], [1, 2], [1, 3], [2, 3], 

[2, 1], [3, 1], [3, 2], [1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1],

would represent all possible effect orders with each effect applied once. 

Passing single Integers is equivalent to passing them in an array.

Data concerning one event can also be given as

(c) a Ref object containing an IdentityDictionary, representing the effect graph. 

Keys of the dictionary – single Integers – are identified with effects of fxData, values

of the dictionary might be single Integers or SequenceableCollections of

Integers, indicating a branching of effects (whenever you want a branching of effects

or routing two different effects to a third one, the input must be given as a ref'd IdentityDictionary).

Within the effect graph an explicit direct out can be denoted with the Symbol \o,

0 represents the source. The graph must be acyclic, a check is performed.

fxOrder can also be a Pattern, sequencing effect orders of the forms (a) - (c).

See principles of operation below and examples 10 (a)-(c).

\cleanupDelay - Number or Pattern to generate Numbers, taken as seconds.

Its meaning differs, depending if source synth (passed by \instrument) has

a gated or fixed length envelope. If a gate control is encountered in the SynthDef

description, a gated envelope is assumed.

In the latter case this arg determines the earliest time after release when

cleanup may start. With a fixed-length envelope it determines the earliest time 

after synth start when cleanup may start.

With effects defined, cleanupDelays of source and effects are summarized.

Defaults to the class variable defaultSourceCleanupDelay which defaults to 0.3.

Typically you would take the releaseTime of the source synth's / synthdef's 

gated envelope, or the maximum overall length of the fixed-length envelope,

each plus a small delta.

\cleanupClock - The clock on which freeing of audio buses is scheduled with SkipJack objects.

As the clock should survive CmdPeriod its permanent flag must be set to true.

Per default the SystemClock is used, but for certain cases, e.g. granulation 

you might want to pass a TempoClock with higher queueSize, see examples below.

\cleanupDt - Number or Pattern to generate Numbers, taken as seconds.

Determines the delta time for cleanup with SkipJack objects. In those intervals 

it is checked whether event-specific cleanup delaytimes have been reached 

and cleanup (freeing buses) should be performed. Defaults to the class variable 

defaultCleanupDt which defaults to 0.2. With higher values more SkipJack objects

have to be scheduled at the same time, although with fewer activity.

\freePerGroup - Boolean, defaults to false.

Determines if effects and zero synths of the chain should be freed separately or

removed at the end of the chain by freeing the enclosing group.

With more effects and longer cleanupDelays the latter leads to a larger number

of parallel synths and might be wasteful thus. On the other hand it can be 

a useful option with short events and cleanupDelays, e.g. in case of granulation. 

See below timing scheme for a more detailled description.


\otherInArgs - Takes SequenceableCollection of Symbols, defaults to nil.

For all passed symbols x the source synth is enabled to read from buses with In.ar(x).

Per default a restrictive check of synth I/O ugens and matching is performed in order to prevent

unintended reading from resp. writing to buses. With this option you can allow reading

in a controlled way, see Ex. 6a and 6b.


fxData - SequenceableCollection(s) of key/value pairs defining effect sequencing or event pattern.

Passing a list saves explicitely typing Pbind, but passing an event pattern is more flexible,

as it allows replacement (Ex.7), event pattern filtering (Ex.3a) and similar operations.


specific keys:


\fx - Symbol or Pattern to generate Symbols.

Determines the effect synthdef.


\cleanupDelay - Number or Pattern to generate Numbers, taken as seconds.

Determines the earliest time after effect node delay when cleanup, 

including freeing the effect synth, may start.

With effects defined, cleanupDelays of source and effects are summarized.

Defaults to the class variable defaultFxCleanupDelay which defaults to 0.05.

Typically you would take the maximum delay of the effect synth plus a small delta. 

\otherInArgs - Takes SequenceableCollection of Symbols, defaults to nil.

For all passed symbols x the effect synth is enabled to read from buses with In.ar(x).

Per default a restrictive check of synth I/O ugens and matching is performed in order to prevent

unintended reading from resp. writing to buses. With this option you can allow reading

in a controlled way, see Ex. 6a and 6b.



*defaultSourceCleanupDelay 

Get the value of this class variable. Defaults to 0.3.


*defaultSourceCleanupDelay_(value)

Set this class variable to value.


*defaultCleanupDt 

Get the value of this class variable. Defaults to 0.2.


*defaultCleanupDt_(value)

Set this class variable to value. 


*defaultFxCleanupDelay 

Get the value of this class variable. Defaults to 0.05.


*defaultFxCleanupDelay_(value)

Set this class variable to value. 





Principle of operation


Per-event effect routing with PbindFx, example scheme of two effects applied sequentially:


This can typically be achieved by passing an array [i, j] to \fxOrder, where i and j denote

arbitrary unequal positive effect numbers (numbers smaller or equal than the size of fxData).


attachments/PbindFx/PbindFx_graph_1.png


Effects FX#1 and FX#2 read from buses BUS#1 and BUS#2 which are reserved for the duration 

of the event plus a cleanup time. BUS#1 and BUS#2 get their data from source synth SRC resp. FX#1. 

"Zero synths" Z#1 and Z#2 are placed before them in node order, they are lasting as long as 

(in fact a bit longer than) FX#1 and FX#2, playing zero signals to BUS#1 and BUS#2 with ReplaceOut and 

cancelling out possible residual signals on those buses, thus blocking feedback. 

BUS#1 and BUS#2 might be multichannel buses, in that case a number of mono zero synths 

is established for each Z#i. 

Two dedicated groups are generated for each event: FXGRP is the container for all

event-generated synths and SRCGRP is placed at its head. Z#1 is placed at the head of

SRCGRP, other zero synths and effects are sequentially placed upwards from the tail

of FXGRP, interlaced in the way described below.

Finally source synth(s) are placed at tail of SRCGRP (the group passed further to the note event).



node ordering for n = 2


FXGRP:

SRCGRP:

Z#1

SRC

Z#2 

FX#1

FX#2



You can also pass a group (or a pattern of groups) to PbindFx, in that case

FXGRP is related to the group depending on \addAction (default \addToHead):



general node ordering with n sequentially applied effects for n > 2 

group GRP passed to PbindFx with key \group, 

default addAction \addToHead


GRP:

  FXGRP:

SRCGRP:

Z#1

SRC

Z#2 

FX#1

...

...

Z#n

FX#n-1

FX#n



Note that effect order is arbitrary per event, determined by key \fxOrder, 

in these examples FX#i denote the order in the regarded event, 

not the order in which the effects are passed to PbindFx.

The whole process is implemented via dedicated event type 'pbindFx', 

an extension of event type 'note', which is chosen with PbindFx by default.

The following schemes refer to server-side timing, additional lang-side offset might be passed with 

lag (in seconds) to ensure proper timing for sequences combining delayed and non-delayed effects 

e.g. including occasional echo as in several of the examples below.



Timing scheme of PbindFx with two effects applied sequentially, 

source synth with gated envelope:



attachments/PbindFx/PbindFx_graph_2a.png

t#0: start SRC, Z#i, FX#i and groups with server-side ordering as described in routing scheme,

in fact Z#i are started slightly earlier.

t#1: begin of SRC envelope release period, caused by a release message (set gate arg to 0)

t#2: end of SRC envelope release period, probably end of source synth (doneAction = 2)

t#3: supposed latest end of source synth due to given cleanupDelay of source, added to t#1

(cleanupDelay might be passed as a constant maximum release time for all events)

t#4: with ~freePerGroup == false (default): freeing of FX#1 and Z#1, the latter a bit later.

with ~freePerGroup == true: do nothing

t#5: free FXGRP, thus also FX#2 and Z#2.


For longer effect chains with n effects the case distinction of t#4 applies to all pairs Z#i / FX#i for 1 < i < n:

with ~freePerGroup == true they all are freed at last by freeing the group.



Timing scheme of PbindFx with two effects applied sequentially, 

source synth with fixed-length envelope:



attachments/PbindFx/PbindFx_graph_2b.png


t#0: start SRC, Z#i, FX#i and groups with server-side ordering as described in routing scheme,

in fact Z#i are started slightly earlier.

t#1: begin of SRC envelope release period, caused by the synth itself (no setting gate to 0)

t#2: end of SRC envelope release period, probably end of source synth (doneAction = 2)

t#3: supposed latest end of source synth due to given cleanupDelay of source, added to t#0

(cleanupDelay might be passed as a constant maximum envelope length for all events)

t#4 - t#5: as with gated envelope



Parallel effect processing, arbitrary effect graphs:


The most simple case of applying two effects in parallel can be triggered by passing `(0: [1, 2]) to fxOrder.

Effect numbers occurring in arrays of dictionary values cause a routing from these effects to out.

Symbol \o (for destination out) can also be passed explicitely.


Branching like this requires the use of a split synth which routes the output to the demanded

multitude of fx buses. A suitable predefined split synthdef is chosen according to the number of branches

and the channel number of the signal to be splitted – currently both is limited by 8, however

synths, which are only playing to out directly, might have an arbitrary number of output channels.


Different to sequential fx processing, both fx bus zero synths have to be added before the source,

additional zero synth(s) for the split bus are prepended.




attachments/PbindFx/PbindFx_graph_3a.png


The following effect graph can be established by fxOrder `(0: [1, 2, 3], 2: [4, \o], 3: 2),

split synths, zero synths (for fx buses and split buses) are omitted in this scheme.

For each (acyclic) fx graph a topological order is calculated (here it could e.g. be [0,1,3,2,4])

and the node ordering, including FXGRP and SRCGRP, similar to the sequential case, 

is derived accordingly. Some small differences, especially concerning the cleanup delay times, 

have to be taken into account, these details are omitted here.


attachments/PbindFx/PbindFx_graph_3b.png


Conventions 


1.) Source instrument and effect SynthDefs must be known to SynthDescLib.global 

(e.g. by creating SynthDefs with methods 'add' or 'store')


2.) Fx SynthDefs must be defined with arg 'out' for the outbus index, arg 'in' for inbus index

and use In.ar(in, ...) within the SynthDef.

It's up to the user whether to define the effect with dry/wet mix option.

Effect synths don't need to have an envelope, their freeing is handled by the event type function.

3.) Effect chains must be defined properly in terms of in/out channel number.

For each event the bus matching of the effect chain is checked, following these conventions:

.) For source and fx SynthDefs there must be only one out ugen using 'out' as bus arg.

.) Source SynthDefs must not have In ugens, except they are admitted by \otherInArgs. 

.) Fxs must read from buses with ugen In.ar(in, ...) refering to the bus arg 'in', 

there must not be other In ugens within the fx SynthDef, except they are admitted by \otherInArgs,

see Exs. 6a and 6b

.) Number of out channels of preceeding source / fx synth must not be greater than 

the number of the following fx synth's in channels, this is checked for all pairs of an fx graph.

.) Mismatches of above points lead to errors.

4.) Checks of (3) are also based on info in SynthDescLib.global. It is possible to replace SynthDefs 

on the fly (Ex. 7a, 7b), in that case I'd recommend to use also methods 'add' or 'store' – 

e.g. in case of using 'send' for redefinition no bus matching check is performed and 

a possibly wrong routing would be undetected.

5.) As shown in above graphics the source synth's cleanupDelay is interpretated differentely 

with gated and non-gated (fixed-length) envelopes. This is done automatically by looking 

for a 'gate' arg in the SynthDef's SynthDesc. It's the users responsibility to use the 

conventional arg 'gate' for release in the SynthDef and omit a 'gate' arg with 

SynthDefs employing fixed-length envelopes.


6.) The source Pbind / Pbind pairs may be defined to produce multiple nodes per event,

then the whole source signal is routed to the fx graph (see Ex. 2b, 2c and others).

On the other hand the same kind of expansion (with arrays produced by key streams)

is not supported for fx patterns, for applying different fxs to parallel events use parallel PbindFxs (Exs. 3a, 3b). 

However it is of course possible that fx synths have array args and fx patterns are passing them 

with the double-bracketing convention used for these cases (see Event patterns and array args).


7.) Source instrument and effect SynthDefs are allowed to have args of same name. 

There is no problem as key/value pairs are stored in separate lists/events.

 

8.) PbindFx is implemented per automatically chosen effect type 'pbindFx', which employs 

event type 'note', thus an event type must neither be passed to pbindData nor to fxData.

9.) The value conversion framework can be used for fxData, e.g. by passing \midinote instead of

\freq or \db instead of \amp as with Pbind, see Ex.9. Other than with Pbind, freq and amp event defaults 

are not passed to fx synths, if no such values are passed via fxData. 

In that case default values of fx synths are taken.

10.) While playing PbindFx you should not play with allocation affecting private buses.

Freeing buses is done by SkipJack objects, so you should not forcefully stop 

all SkipJack objects while playing PbindFx.


11.) Keys \group and \addAction may be passed, they determine how the group enclosing 

event-generated synths is related to the passed group, see scheme 'Principles of operation'.

12.) Zero synths are using the SynthDefs 'pbindFx_zero' and 'pbindFx_splitZero' which 

are written to disk at startup time, as well as split synths pbindFx_split_axb for a = 2,...,8

and b = 1,...,8. Of course these SynthDefs shouldn't be deleted or exchanged.


Resources, troubleshooting


1.) You might encounter the error message "Meta_Bus:audio: failed to get an audio bus allocated." 

As audio buses for effect chains are allocated and freed per event a higher number of 

private audio buses is likely to be required (more than default 128). 

You might also encounter the error message "exception in real time: alloc failed, 

increase server's memory allocation (e.g. via ServerOptions)".

Hence it's recommended to set the concerned server options before (re-)booting and working with PbindFx, e.g.:

(

s.options.numPrivateAudioBusChannels = 1024;

s.options.memSize = 8192 * 16;

s.reboot;

)


2.) You might encounter the warning "Scheduler queue is full."

Per default delayed freeing of buses is scheduled on SystemClock, which defaults to queueSize 1024.

In case of granulation and/or large cleanup delays it's recommended to pass a cleanup clock with

sufficiently large queueSize via pbindData, its permanent flag must be set to true, e.g.:

...

\cleanupClock, t = TempoClock(queueSize: 8192).permanent_(true) 

...

Note that this clock keeps on running and survives CmdPeriod, you should explicitely stop it

if you don't need it anymore, hence it should be stored in a variable. In case you haven't done that

you can still stop all TempoClocks and remove them from CmdPeriod with

TempoClock.all.copy.do(_.stop)

3.) Cleanup delays for source and effects (or, if not passed, their default values) should be sufficiently large

(see description of \cleanupDelay), otherwise effects and audio buses might be freed too early

and signals cut.


4.) Some effects, like echo, introduce a delay. If sequencing mixes delayed events

with non-delayed events, entries of the latter have to be delayed accordingly to preserve correct timing, 

this can be done by setting an offset in seconds with \lag. See examples with echo below.




Example 1: Straight usage with unchanged effect order


Example 1a: Source synth with sustained envelope



// boot server with extended resources


(

s.options.numPrivateAudioBusChannels = 1024;

s.options.memSize = 8192 * 16;

s.reboot;

)


// basic source and effect synthdefs for this help file


(

// All ins and outs use two channels


// source synthdef

// take releaseTime = decayTime


SynthDef(\source, { |out = 0, freq = 400, decayTime = 0.5,

attackTime = 0.005, amp = 0.1, gate = 1|

var env, sig = Decay.ar(Impulse.ar(0), decayTime, Saw.ar(freq));

env = EnvGen.ar(Env.asr(attackTime, amp, decayTime, \lin), gate, doneAction: 2);

Out.ar(out, sig ! 2 * env)

}).add;



// spat fx

// This effect introduces a very small delay,

// in examples balancing by lag (as it obviously has to be done with echo) is neglected.


SynthDef(\spat, { |out, in, freq = 1, maxDelayTime = 0.005,

amp = 1, mix = 1|

var sig, inSig = In.ar(in, 2);

sig = DelayC.ar(

inSig,

maxDelayTime,

{ LFDNoise3.ar(freq, maxDelayTime, maxDelayTime/2) } ! 2,

amp

);

Out.ar(out, (1 - mix) * inSig + (sig * mix));

}).add;



// echo fx, always unified delay maxEchoDelta


SynthDef(\echo, { |out, in, maxEchoDelta = 0.2, echoDelta = 0.1,

decayTime = 1, amp = 1, mix = 1|

var sig, inSig = In.ar(in, 2);

sig = DelayL.ar( 

CombL.ar(inSig, maxEchoDelta, echoDelta, decayTime, amp), 

maxEchoDelta, 

maxEchoDelta - echoDelta

);

Out.ar(out, (1 - mix) * inSig + (sig * mix));

}).add;



// wah-wah fx


SynthDef(\wah, { |out, in, resLo = 200, resHi = 5000,

cutOffMoveFreq = 0.5, rq = 0.1, amp = 1, mix = 1|

var sig, inSig = In.ar(in, 2);

sig = RLPF.ar(

inSig,

LinExp.kr(LFDNoise3.kr(cutOffMoveFreq), -1, 1, resLo, resHi),

rq,

amp

).softclip;

Out.ar(out, (1 - mix) * inSig + (sig * mix));

}).add;



// reverb fx

// rough estimation: freeVerb's room arg = decayTime / 10


SynthDef(\reverb, { |out, in, damp = 0.5,

decayTime = 10, amp = 1, mix = 1|

var sig, inSig = In.ar(in, 2);

Out.ar(out, FreeVerb.ar(inSig, mix, min(decayTime, 10) / 10, damp, amp));

}).add;

)



// Fx params are sequenced on a per-event base.


// see server window: number of groups (divided by 2) indicates 

// the number of parallel event chains in action

// check while running with s.queryAllNodes


(

p = PbindFx([

\instrument, \source,

\dur, 0.25,

\amp, 0.2,

\midinote, Prand([

Pwhite(80, 90, 1),

Prand([60, 67, 70, 73]) + Prand([0, -12.3, -23.7], inf),

], inf),


\fxOrder, [1, 2], 

// With a sustained envelope \cleanupDelay refers to the maximum release time,

// in SynthDef \source releaseTime = decayTime, so take cleanupDelay = decayTime

\decayTime, Pwhite(0.2, 2),

\cleanupDelay, Pkey(\decayTime)

],[

\fx, \spat,

// oscillation of delay -> frequency modulation of source signal

\freq, Prand([1, 2, 3], inf),

\maxDelayTime, 0.005,

\cleanupDelay, Pkey(\maxDelayTime)

],[

// variation by sequencing of params

\fx, \wah,

\mix, Pseq([0.2, 0.5, 0.7], inf),

\cutOffMoveFreq, Pseq([1, 2, 5, 10], inf),

\cleanupDelay, 0.01

]

);


q = p.play;

)


// see server window and compare "regular" stop 

// (descending number of groups, synths and ugens reflects delayed cleanup)

// with stopping the same example by Cmd-Period


q.stop;



Example 1b: Source synth with fixed-length envelope


// SynthDefs from Ex. 1a plus

// SynthDef variation with adsr args


(

SynthDef(\source_adsrFixed, { |out = 0, freq = 400, decayTime = 0.5, 

att = 0.005, dec = 0.01, sus = 0.2, rel = 0.3, susLevel = 0.5, amp = 0.1|

var env, sig = Saw.ar(freq);

env = EnvGen.kr(Env([0, 1, susLevel, susLevel, 0], [att, dec, sus, rel]), doneAction: 2);

Out.ar(out, sig ! 2 * env * amp)

}).add;

)


// adsr values passed, \cleanupDelay estimated as max of sum


(

p = PbindFx([

\instrument, \source_adsrFixed,

\dur, 0.25,

\att, Pwhite(0.005, 0.01),

\dec, Pwhite(0.01, 0.02),

\sus, Pwhite(0.02, 0.3),

\rel, Pwhite(0.2, 1.5),


\susLevel, 0.4,

\amp, 0.2,


\midinote, Prand([

Pwhite(80, 90, 1),

Prand([60, 67, 70, 73]) + Prand([0, -12.3, -23.7], inf),

], inf),


\fxOrder, [1, 2], 

// cleanupDelay must be larger than max env length, otherwise events might be cut !

// here we do an estimation (att + dec + sus + rel < 2),

// but it could be summed from those event values too, e.g. with

// \cleanupDelay, Pfunc { |e| e.att + e.dec + e.sus + e.rel }

\cleanupDelay, 2

],[

\fx, \spat,

\freq, Prand([1, 2, 3], inf),

\maxDelayTime, 0.005,

\cleanupDelay, Pkey(\maxDelayTime)

],[

\fx, \wah,

\mix, Pseq([0.2, 0.5, 0.7], inf),

\cutOffMoveFreq, Pseq([1, 2, 5, 10], inf),

\cleanupDelay, 0.01

]

);


q = p.play;

)



// check node order while running


s.queryAllNodes;


q.stop;



Example 2: Sequencing of different effect chains


Example 2a: Basic usage


// PbindFx using spat and echo effects.

// Especially relevant keys here are \fxOrder which determines fx sequencing

// and \cleanupDelay for proper releaseTimes of source and effects


// SynthDefs from Ex. 1a, see also extended server resources defined there


(

p = PbindFx([

\instrument, \source,

\dur, 0.5,

\amp, 0.3,

\midinote, Pwhite(50, 90),


// fx sequence \spat, \spat + \echo, etc.

\fxOrder, Pseq([1, [1,2]], inf),


// echo is delayed (maxEchoDelta = 0.2),

// compensate here by shift when no echo

// we need \lag rather than \timingOffset as the whole delaytime calculation refers to seconds

\lag, Pseq([0.2, 0], inf),


// in SynthDef \source releaseTime = decayTime, so take cleanupDelay = decayTime

\decayTime, Pseq([1, 0.1], inf),

\cleanupDelay, Pkey(\decayTime)

],[

// define effect with index 1

\fx, \spat,

\maxDelayTime, 0.005,


// oscillation of delay -> frequence modulation of source signal

\freq, Pseq([1, 1, 10], inf),


\cleanupDelay, Pkey(\maxDelayTime)

],[

// define effect with index 2

\fx, \echo,

\echoDelta, 0.08,

\decayTime, Pwhite(0.8, 3),


\cleanupDelay, Pkey(\decayTime)

]

);


q = p.play;

)


q.stop;



Example 2b: Data sharing between source and effect streams


// If more than one synth per event is produced (here by key 'midinote'), 

// the effect chain is applied to all of them, see Ex. 3 for applying 

// different effects to parallel synths.


// SynthDefs from Ex. 1a, see also extended server resources defined there


(

p = PbindFx([

\instrument, \source,

\dur, 0.2,

\amp, 0.3,


// downwards tendency + chord sequence

\midinote, Pseq((90, 80..50), inf) +

Pn(Pshuf([[0, 5], 0, [0, 2.5], [-2.5, 12.5], [-3, 0]])),


\fxOrder, Prand([1, [1,2], [1,2]], inf),


// lag must be adapted to maxEchoDelta

\lag, Pfunc { |e| e.fxOrder.isArray.if { 0 }{ 0.2 } },


// echo -> shorter source decay

\decayTime, Pfunc { |e| e.fxOrder.isArray.if { 0.1 }{ 0.7 } },

\cleanupDelay, Pkey(\decayTime)

],[

\fx, \spat,

\maxDelayTime, 0.005,

\cleanupDelay, Pkey(\maxDelayTime)

],[

\fx, \echo,

\echoDelta, Pseq((1..5)/50, inf),

\decayTime, 1,

\cleanupDelay, Pkey(\decayTime)

]

);


q = p.play;

)


q.stop;



Example 2c: Some extensions


// Additional use of rests, reverb added.

// Here the reverb usage is deliberately wasteful, see Ex.2d for an alternative.

// The use of Pn + Pshuf (or equivalently Pshufn) gives 

// balanced random variation for several key streams


// SynthDefs from Ex. 1a, see also extended server resources defined there


(

p = PbindFx([

\instrument, \source,

\dur, Pn(Pshuf(0.2!5 ++ Rest(0.2))),


\midinote, Pseq((90, 80..40), inf) +

Pn(Pshuf([[0, 5], 0, [0, 2.5], [-2.5, 12.5], [-3, 0]])),


\fxOrder, Pn(Pshuf([[1,2], [1,2,3], [3,1], 1])),


\lag, Pfunc { |e| e.fxOrder.asArray.includes(2).if { 0 }{ 0.2 } },

\amp, Pfunc { |e| (e.fxOrder != [1,2]).if { 0.3 }{ 0.6 } },


\decayTime, Pfunc { |e| 

rrand(0.3, 0.8) / (e.fxOrder.asArray.includes(2).if { 10 }{ 1 }) 

},

\cleanupDelay, Pkey(\decayTime)

],[

\fx, \spat,

\freq, Pn(Pshuf([1, 1, 1, 5, 20, 50])),

\maxDelayTime, 0.005,

\cleanupDelay, Pkey(\maxDelayTime)

],[

\fx, \echo,

\echoDelta, Pseq((1..5)/50, inf),

\decayTime, Pwhite(0.3, 1.8),

\cleanupDelay, Pkey(\decayTime)

],[

\fx, \reverb,

\mix, 0.3,

\damp, 0.1,

\decayTime, Pwhite(3.0, 10),

\cleanupDelay, Pkey(\decayTime)

]

);


q = p.play;

)


q.stop;



Example 2d: Saving resources 


// If effects have a long cleanup delay, you will get a possibly large number of

// overlapping effect chains. E.g. in Ex. 2c many reverb synths can be there in

// parallel, the decayTime is controlled by Pwhite(3.0, 10), so it might well be

// that reverbs with decayTimes 7.95, 8, and 8.1 are instantiated in parallel,

// which doesn't make much difference and is quite wasteful.

// Reverb is often placed at the last position of the effect chain,

// so a more efficient approach would be the following: do

// all effect sequencing without reverb with PbindFx and pipe

// the overall out to a permanently running reverb.



// SynthDefs from Ex. 1a, see also extended server resources defined there


// start two reverbs with different parameters, read from dedicated buses


(

a = Bus.audio(s, 2);

b = Bus.audio(s, 2);


x = Synth(\reverb, [mix: 0.3, damp: 0.1, decayTime: 3, in: a]);

y = Synth(\reverb, [mix: 0.2, damp: 0.1, decayTime: 10, in: b]);

)

  

// play PbindFx 

// compare CPU usage with Ex.2c


(

p = PbindFx([

\instrument, \source,

\dur, Pn(Pshuf(0.2!5 ++ Rest(0.2))),


\midinote, Pseq((90, 80..40), inf) +

Pn(Pshuf([[0, 5], 0, [0, 2.5], 0, [-2.5, 12.5], [-3, 0]])),


\fxOrder, Pn(Pshuf([1, 2, [1,2]])),


\lag, Pfunc { |e| e.fxOrder.asArray.includes(2).if { 0 }{ 0.2 } },

\amp, Pfunc { |e| (e.fxOrder != [1,2]).if { 0.3 }{ 0.6 } },


\decayTime, Pfunc { |e| 

rrand(0.3, 0.8) / (e.fxOrder.asArray.includes(2).if { 10 }{ 1 }) 

},

\cleanupDelay, Pkey(\decayTime),


// pipe out to different reverbs resp. 0 (no reverb)

\out, Pn(Pshuf([0, 0, a, a, b]))

],[

\fx, \spat,

\freq, Pn(Pshuf([1, 1, 1, 5, 20, 50])),

\maxDelayTime, 0.005,

\cleanupDelay, Pkey(\maxDelayTime)

],[

\fx, \echo,

\echoDelta, Pseq((1..5)/50, inf),

\decayTime, Pwhite(0.3, 1.8),

\cleanupDelay, Pkey(\decayTime)

]

);


q = p.play;

)


// stop


q.stop;


// free extra resources


[x, y, a, b].do(_.free);




Example 3: Different effects for parallel synths


// This can be done with parallel PbindFxs


Example 3a: Using a template Pbind


// Here the option of passing a source Pbind instead of a list of Pbind pairs can be used.

// SynthDefs from Ex. 1a, see also extended server resources defined there


(

// master pattern, fxOrders defines fxOrder for voices of chord

f = Pbind(

\dur, 0.3,

\type, \rest,

\fxOrders, Pn(Pshuf([

[[1, 3], 2, 1], [1, [1, 3], 2], [2, 1, [1, 3]],

[0, 1, 1], [1, 0, 1], [1, 1, 0]

]).collect { |o| ~o = o })

);


// core source Pbind

a = Pbind(

\instrument, \source,

\dur, 0.3,


// reference to fxOrder will be got from Pchains below

\lag, Pfunc { |e| e.fxOrder.includes(2).if { 0 }{ 0.2 } },

\amp, Pfunc { |e| e.fxOrder.any([2,3].includes(_)).if { 0.3 }{ 0.1 } },


\decayTime, Pfunc { |e| rrand(0.5, 0.7) / (e.fxOrder.includes(2).if { 10 }{ 1 }) },

\cleanupDelay, Pkey(\decayTime)

);


// lists of fx pairs

b = [[

\fx, \spat,

\freq, Pn(Pshuf([1, 2, 3, 10])),

\maxDelayTime, 0.005,

\cleanupDelay, Pkey(\maxDelayTime)

],[

\fx, \echo,

\echoDelta, 3/50,

\decayTime, Pwhite(0.3, 1.8),

\cleanupDelay, Pkey(\decayTime)

],[

\fx, \wah,

\mix, 0.5,

\cutOffMoveFreq, Pseq([5, 10], inf),

\cleanupDelay, 0.01

]];


// derive three Pchains from core Pbind, 

// voice-specific fxOrder will be read from master pattern

u = a <> Pbind(

\fxOrder, Pfunc { ~o[0].asArray },

\midinote, 72

);


v = a <> Pbind(

\fxOrder, Pfunc { ~o[1].asArray },

\midinote, Pstutter(Pwhite(5, 10), Prand((60..70), inf))

);


w = a <> Pbind(

\fxOrder, Pfunc { ~o[2].asArray },

\midinote, Pstutter(Pwhite(5, 10), Prand((75..85), inf))

);



// play in parallel, master must be before


d = 0.001;


q = Ptpar([

0, f,

d, PbindFx(u, *b),

d, PbindFx(v, *b),

d, PbindFx(w, *b)

]).play;

)


q.stop;




Example 3b: Using a PbindFx generator Function


// Equivalent to Ex. 3a, but might look more straight, as 

// fxOrder pair already generated with PbindFx Function.


// SynthDefs from Ex. 1a, see also extended server resources defined there


(

// master pattern for fxOrder

f = Pbind(

\dur, 0.3,

\type, \rest,

\fxOrders, Pn(Pshuf([

[[1, 3], 2, 1], [1, [1, 3], 2], [2, 1, [1, 3]],

[0, 1, 1], [1, 0, 1], [1, 1, 0]

]).collect { |o| ~o = o })

);


// PbindFx generator


g = { |i| PbindFx([

\instrument, \source,

\dur, 0.3,


\fxOrder, Pfunc { ~o[i].asArray },

\lag, Pfunc { |e| e.fxOrder.includes(2).if { 0 }{ 0.2 } },

\amp, Pfunc { |e| e.fxOrder.any([2,3].includes(_)).if { 0.3 }{ 0.1 } },


\decayTime, Pfunc { |e| rrand(0.5, 0.7) / (e.fxOrder.includes(2).if { 10 }{ 1 }) },

\cleanupDelay, Pkey(\decayTime),

],[

\fx, \spat,

\freq, Pn(Pshuf([1, 2, 3, 10])),

\maxDelayTime, 0.005,

\cleanupDelay, Pkey(\maxDelayTime)

],[

\fx, \echo,

\echoDelta, 3/50,

\decayTime, Pwhite(0.3, 1.8),

\cleanupDelay, Pkey(\decayTime)

],[

\fx, \wah,

\mix, 0.5,

\cutOffMoveFreq, Pseq([5, 10], inf),

\cleanupDelay, 0.01

]

);

};


// derive three Pbindfs from PbindFx generator,

// as PbindFx is a subclass of Pbind, you can apply Pbindf as to Pbind


u = Pbindf(g.(0), \midinote, 72);

v = Pbindf(g.(1), \midinote, Pstutter(Pwhite(5, 10), Prand((60..70), inf)));

w = Pbindf(g.(2), \midinote, Pstutter(Pwhite(5, 10), Prand((75..85), inf)));


d = 0.001;

q = Ptpar([0, f, d, u, d, v, d, w]).play;

)


q.stop;




Example 4: Using one fx SynthDef in more than one fxData


// For using the same effect synth in one effect chain more than once,

// but with different parameters, pass it with an additional fxData.


// SynthDefs from Ex. 1a, see also extended server resources defined there


(

p = PbindFx([

\instrument, \source,

\dur, 1/3,

\amp, Pseq([0.2, 0.4, 0.3], inf),

\midinote, Pseq([

Pwhite(35, 48, 1) + [-14, 0, 14],

Pshuf([60, 67, 70, 73], 1) + Pshuf([0, 12], 1) + [0, 4, 12],

], inf),


\fxOrder, Pseq([1, [1, 2], [1, 2, 3]], inf),

\lag, Pseq([0.17, 0.03, 0.00], inf),


\decayTime, Pseq([2.5, 0.1, 0.1], inf),

\cleanupDelay, Pkey(\decayTime)

],[

\fx, \spat,

\freq, Prand([1, 2, 3], inf),

\maxDelayTime, 0.005,

\cleanupDelay, Pkey(\maxDelayTime)

],[

\fx, \echo,

\echoDelta, Pwhite(0.06, 0.1),

\decayTime, 1,


\cleanupDelay, Pkey(\decayTime)

],[

\fx, \echo,

// short delta results in additional frequency

\echoDelta, Pwhite(0.005, 0.01),

\decayTime, 0.2,


\cleanupDelay, Pkey(\decayTime)

]

);


q = p.play;

)


q.stop;




Example 5: Tempo control


// Tempo control works as with Pbind and can be 

// influenced by a number of parameters.


// As cleanup parameters of PbindFx are passed in seconds,

// tempo control can be done independent from making cleanup time changes

// (though you might do so as well)


// The use of a dedicated TempoClock as master tempo control is a good idea.

// Setting tempo for individual streams can be done with 'stretch'


// The following example employs a PbindFx generator Function,

// the tempo of streams can be controlled individually and generally.


// SynthDefs from Ex. 1a, see also extended server resources defined there


(

// PbindFx generator Function,

// streams will get different midinote offsets,

// tempo will be read from a passed array


g = { |midiOffset, tempoArray, index|


PbindFx([

\instrument, \source,

\dur, 0.25,

\stretch, Pfunc { 1 / tempoArray[index] },

\midinote, Prand([

Pwhite(55, 75, 1),

Pn(Pshuf([60, 67, 70, 73])),

], inf) + midiOffset,


\fxOrder, Pn(Pshuf([1, 1, [1, 2], [1, 3]])),

\lag, Pfunc { |e| e.fxOrder.asArray.includes(2).if { 0 }{ 0.2 } },

\amp, 0.2,


\decayTime, Pfunc { |e| rrand(0.3, 0.8) / (e.fxOrder.asArray.includes(2).if { 10 }{ 1 }) },

\cleanupDelay, Pkey(\decayTime)

],[

\fx, \spat,

\freq, 2,

\maxDelayTime, 0.005,

\cleanupDelay, Pkey(\maxDelayTime)

],[

\fx, \echo,

\echoDelta, Pseq((1..5)/50, inf),

\decayTime, Pwhite(0.3, 1.8),

\cleanupDelay, Pkey(\decayTime)

],[

\fx, \wah,

\mix, 0.5,

\resLo, 400,

\reHi, 2000,

\cutOffMoveFreq, 10,

\cleanupDelay, 0.01

]

) };


// master TempoClock


t = TempoClock.new;



// array for individual tempo factors


~tempo = [1, 1, 2]/2;


// start first player on quant grid,

// it refers to the tempo factor ~tempo[0]

// note that quant refers to the tempo of the TempoClock


x = g.(-24, ~tempo, 0).play(t, quant: 1);

)


// start other players on quant grid


y = g.(0, ~tempo, 1).play(t, quant: 1);


z = g.(12, ~tempo, 2).play(t, quant: 1);



// change player's individual tempos,

// note that tempo changes do not necessarily happen on quant grid,

// so a rhythmic "phase shift" (which can be nice) might happen


~tempo[2] = 2/3


~tempo[1] = 1/3


~tempo[0] = 1



// set general tempo


t.tempo = 3/4



// stop players individually


x.stop;


y.stop;


z.stop;



// stop clock


t.stop;





Example 6: Further external routing


// This means the use of buses, which are not internally used by PbindFx's event Function.

// As shown in Ex.2d it can be useful to route PbindFx's out to an external reverb,

// Vice versa data can be read from external buses, from source synths as well as from fx synths.

// Audio routing benefits from the possibility to pass groups to PbindFx.

// Then all temporary groups, generated during playing the PbindFx,

// are enclosed by it and node order can be clearly defined.

// For audio bus routing better use In.ar than mapping with asMap,

// that way matching of ins and outs of fx chains is checked and it avoids 

// issues with using asMap and stopping the external source (occuring at least in SC 3.6.6). 



Example 6a: Source synth reading audio modulation signals from external buses


// source synth based on SinOsc and additional ring modulation,

// while reading from an audio bus with no signal, it outputs only the sine


(

SynthDef(\source_mod, { |out = 0, freq = 400, decayTime = 0.5,

attackTime = 0.005, amp = 0.1, modIn, gate = 1|

var env, sig;

sig = Decay.ar(Impulse.ar(0), decayTime, SinOsc.ar(freq) * (1 + In.ar(modIn)));

env = EnvGen.ar(Env.asr(attackTime, amp, decayTime, \lin), gate, doneAction: 2);

Out.ar(out, sig ! 2 * env)

}).add;

)


// SynthDefs from Ex. 1a, see also extended server resources defined there


(

// new group and bus for the modulation signal

g = Group.new;

a = Bus.audio(s, 1);


// play source with spat effect only

p = PbindFx([

\instrument, \source_mod,

\dur, 0.25,

\amp, 0.1,

\midinote, Prand([

Pwhite(80, 90, 1),

Prand([60, 67, 70, 73]) + Prand([0, -12.3, -23.7], inf),

], inf),

\fxOrder, 1, 


\modIn, a,

// to enable this reading the fx chain check must know

\otherInArgs, [\modIn],

\group, g,


\decayTime, Pwhite(0.2, 2),

\cleanupDelay, Pkey(\decayTime)

],[

\fx, \spat,

\freq, Prand([1, 2, 10], inf),

\maxDelayTime, 0.005,

\cleanupDelay, Pkey(\maxDelayTime)

]

);


q = p.play;

)



// while playing PbindFx, play modulation synth to dedicated bus,

// placing before group ensures that all generated synths can read from the bus


x = { Out.ar(a, SinOsc.ar(500, 0, 0.5)) }.play(target: g, addAction: \addBefore);


// no modulation again


x.free;



// modulation with other signal


y = { Out.ar(a, Pulse.ar(700, 0.5, 0.5)) }.play(target: g, addAction: \addBefore);



// add a further modulation signal


z = { Out.ar(a, Formant.ar(700, 1200, mul: 0.5)) }.play(target: g, addAction: \addBefore);



// formant modulation only


y.free;


// stop it also


z.free;



// stop and PbindFx cleanup


q.stop;



// cleanup of other resources, free bus and group


(

g.free;

a.free;

)



Example 6b: Fx synths reading audio modulation signals from external buses


(

// simple sine source synth

SynthDef(\source_sine, { |out = 0, freq = 400, decayTime = 0.5,

attackTime = 0.005, amp = 0.1, gate = 1|

var env, sig;

sig = Decay.ar(Impulse.ar(0), decayTime, SinOsc.ar(freq));

env = EnvGen.ar(Env.asr(attackTime, amp, decayTime, \lin), gate, doneAction: 2);

Out.ar(out, sig ! 2 * env)

}).add;


// ring modulation fx synth, expecting to read the modulation signal from a bus

SynthDef(\ring, { |out, in, modIn, amp = 2, mix = 0.5|

var sig, inSig = In.ar(in, 2);

sig = inSig * In.ar(modIn, 1);

Out.ar(out, (1 - mix) * inSig + (sig * mix));

}).add;

)


// spat SynthDef from Ex. 1a, see also extended server resources defined there


(

// new group and buses for the modulation signal

g = Group.new;


a = Bus.audio(s, 1);

b = Bus.audio(s, 1);


// two ring modulation fxs

p = PbindFx([

\instrument, \source_sine,

\dur, 0.25,

\amp, 0.12,

\midinote, Prand([

Pwhite(80, 90, 1),

Prand([60, 67, 70, 73]) + Prand([0, -12.3, -23.7], inf),

], inf),


\group, g,


\fxOrder, Pn(Pshuf([1, [1, 2], [1, 3]])),

\decayTime, Pwhite(0.2, 2),

\cleanupDelay, Pkey(\decayTime)

],[

\fx, \spat,

\freq, Prand([1, 2, 10], inf),

\maxDelayTime, 0.005,

\cleanupDelay, Pkey(\maxDelayTime)

],[

\fx, \ring,

// need the index explicitely here

\modIn, a.index,

// to enable this reading the fx chain check must know

\otherInArgs, [\modIn],

\maxDelayTime, 0.005,

\cleanupDelay, Pkey(\maxDelayTime)

],[

\fx, \ring,

\modIn, b.index,

// to enable this reading the fx chain check must know

\otherInArgs, [\modIn],

\maxDelayTime, 0.005,

\cleanupDelay, Pkey(\maxDelayTime)

]

);


q = p.play;

)



// now live change of modulation as in Ex. 6a, but this applies only to one of three events


x = { Out.ar(a, SinOsc.ar(500, 0, 0.5)) }.play(target: g, addAction: \addBefore);



// also modulate events where fx reads from b


y = { Out.ar(b, Pulse.ar(700, 0.5, 0.5)) }.play(target: g, addAction: \addBefore);



// add a further modulation signal


z = { Out.ar(a, Formant.ar(700, 1200, mul: 0.5)) }.play(target: g, addAction: \addBefore);



// remove two others


x.free;


y.free;



// stop it also


z.free;



// stop and PbindFx cleanup


q.stop;



// cleanup of other resources, free buses and group


(

g.free;

a.free;

b.free;

)



Example 6c: Controlling effects with LFOs


// control the amount / mix of an effect continously


// SynthDefs from Ex. 1a, see also extended server resources defined there


(

// bus for LFO control

c = Bus.control(s, 1);


// play source with spat and wah-wah effect,

// as initially bus value is zero there is no wah at start


p = PbindFx([

\instrument, \source,

\dur, 0.25,

\amp, 0.15,

\midinote, Prand([

Pwhite(80, 90, 1),

Prand([60, 67, 70, 73]) + Prand([0, -12.3, -23.7], inf),

], inf),

\fxOrder, [1, 2], 

\decayTime, Pwhite(0.2, 2),

\cleanupDelay, Pkey(\decayTime)

],[

\fx, \spat,

\freq, 2, 

\maxDelayTime, 0.005,

\cleanupDelay, Pkey(\maxDelayTime)

],[

\fx, \wah,

// maps each fx synth's mix input to the control bus

\mix, c.asMap,

// other params might still be controlled per event

\cutOffMoveFreq, Pseq([5, 20], inf),

\cleanupDelay, 0.01

]

);


q = p.play;

)


// chime in with wah from 0 (start phase == -pi/2)


x = { Out.kr(c, SinOsc.kr(0.15, -pi/2).range(0, 0.8)) }.play



// stop and free resources


(

q.stop;

x.free;

c.free;

)



Example 7: Replacement


// Replacement can affect certain key streams only or whole source resp.

// fx patterns, the latter can be done with using the option of 

// passing source / fx patterns instead of lists.


Example 7a: Replacement restricted to key streams


// This can be done with Pbind + Pdefn or Pbind + PL,

// a specific possibility with PbindFx is the replacement of \fxOrder


// SynthDefs from Ex. 1a, see also extended server resources defined there


(

~midi = PLseq([60, 60, 60, 62]);

~fxs = PLseq([0, 0, 1, 3]);


// Pdefn(\midi, Pseq([60, 60, 60, 62], inf));

// Pdefn(\fxs, Pseq([0, 0, 1, 3], inf));


p = PbindFx([

\instrument, \source,

\dur, 0.25,

\midinote, PL(\midi), // Pdefn(\midi),

\fxOrder, PL(\fxs), // Pdefn(\fxs),


\lag, Pfunc { |e| e.fxOrder.asArray.includes(2).if { 0 }{ 0.2 } },

\amp, 0.15,


\decayTime, 0.2,

\cleanupDelay, Pkey(\decayTime)

],[

\fx, \spat,

\freq, 2,

\maxDelayTime, 0.005,

\cleanupDelay, Pkey(\maxDelayTime)

],[

\fx, \echo,

\echoDelta, 0.06,

\decayTime, Pwhite(0.3, 1.8),

\cleanupDelay, Pkey(\decayTime)

],[

\fx, \wah,

\cutOffMoveFreq, Pseq([5, 20], inf),

\cleanupDelay, 0.01

]

);


q = p.play;

)



// exchange midinote and effect sequencing on the fly


~midi = PLseq([60, 62, 63]);


~fxs = PLseq([1, [1, 2], [1, 3], [1, 2, 3]]);


(

~midi = PLshufn([60, 62, 63]) +

PLshufn([-24, -12, 0]) +

PLshufn([0, [0, 7], [0, 7, 12]]);

)



// stop and free resources


q.stop;



Example 7b: Replacement of source and fx patterns


// SynthDefs from Ex. 1a, see also extended server resources defined there


(

// define source and effect patterns


~midi = PLseq([60, 60, 62, 63]);

~fxs = PLseq([1, [1, 2], [1, 3], [1, 2, 3]]);


~src = Pbind(

\instrument, \source,

\dur, 0.25,

\midinote, PL(\midi),

\fxOrder, PL(\fxs),


\lag, Pfunc { |e| e.fxOrder.asArray.includes(2).if { 0 }{ 0.2 } },

\amp, 0.15,


\decayTime, 0.2,

\cleanupDelay, Pkey(\decayTime)

);



~fx1 = Pbind(

\fx, \spat,

\freq, 2,

\maxDelayTime, 0.005,

\cleanupDelay, Pkey(\maxDelayTime)

);


~fx2 = Pbind(

\fx, \echo,

\echoDelta, 0.06,

\decayTime, Pwhite(0.3, 1.8),

\cleanupDelay, Pkey(\decayTime)

);


~fx3 = Pbind(

\fx, \wah,

\cutOffMoveFreq, Pseq([5, 20], inf),

\cleanupDelay, 0.01

);


// pass PLx or Pdef placeholder patterns to PbindFx

p = PbindFx(PL(\src), PL(\fx1), PL(\fx2), PL(\fx3));


q = p.play;

)




(

// chorus fx


SynthDef(\chorus, { |out, in, amp = 1, loDelay = 0.001, hiDelay = 0.005,

maxDelayTime = 0.1, mix = 1|

var sig, inSig = In.ar(in, 2);

inSig = Mix.fill(10, { |i|

DelayL.ar(inSig, maxDelayTime, LFDNoise3.ar(2).range(loDelay, hiDelay))

});

sig = inSig * amp / 2;

Out.ar(out, (1 - mix) * inSig + (sig * mix));

}).add;

)


// replace on the fly, fxOrder sequencing stays the same, but effect changes


(

~fx3 = Pbind(

\fx, \chorus,

\maxDelayTime, 0.01,

\loDelay, 0.001,

\hiDelay, 0.01,

\cleanupDelay, Pkey(\maxDelayTime)

);

)


(

// new source SynthDef


SynthDef(\source_pulse, { |out = 0, freq = 400, decayTime = 0.5,

attackTime = 0.005, amp = 0.1, gate = 1|

var env, sig;

sig = Decay.ar(Impulse.ar(0), decayTime, Pulse.ar(freq));

env = EnvGen.ar(Env.asr(attackTime, amp, decayTime, \lin), gate, doneAction: 2);

Out.ar(out, sig ! 2 * env)

}).add;

)


// replace source, building of phrases with rests


(

~src = Pbind(

\instrument, \source_pulse,

\dur, Pn(Pshuf([1, 1, 2, 2, 2, 2, 2, 4, Rest(5)], inf)) / 8,

\midinote, PL(\midi) + Pn(Pshuf([-24, -19, 0, 19, 24], inf)),

\fxOrder, PL(\fxs),


\lag, Pfunc { |e| e.fxOrder.asArray.includes(2).if { 0 }{ 0.2 } },

\amp, 0.1,


\decayTime, 0.1,

\cleanupDelay, Pkey(\decayTime)

);

)


q.stop;



Example 7c: Replacement with Pbindef


// SynthDefs from Ex. 1a, see also extended server resources defined there


(

// source and fxs passed as Pbindefs


// list of instrument symbols

i = [\src, \fx1, \fx2, \fx3];


Pbindef(\src,

\instrument, \source,

\dur, 0.25,

\midinote, Pseq([60, 60, 60, 62], inf),

\fxOrder, Pseq([0, 0, 1, 3], inf),


\lag, Pfunc { |e| e.fxOrder.asArray.includes(2).if { 0 }{ 0.2 } },

\amp, 0.15,


\decayTime, 0.2,

\cleanupDelay, Pkey(\decayTime)

);


Pbindef(\fx1,

\fx, \spat,

\freq, 2,

\maxDelayTime, 0.005,

\cleanupDelay, Pkey(\maxDelayTime)

);


Pbindef(\fx2,

\fx, \echo,

\echoDelta, 0.06,

\decayTime, Pwhite(0.3, 1.8),

\cleanupDelay, Pkey(\decayTime)

);


Pbindef(\fx3,

\fx, \wah,

\cutOffMoveFreq, Pseq([5, 20], inf),

\cleanupDelay, 0.01

);



p = PbindFx(*i.collect { |x| Pbindef(x) });


q = p.play;

)



// replace some of source's key streams


(

Pbindef(\src,

\midinote, Pn(Pshuf([60, 62, 63])) +

Pn(Pshuf([-24, -12, 0])) +

Pn(Pshuf([0, [0, 7], [0, 7, 12]])),

\fxOrder, Pseq([[1, 3], [1, 2, 3]], inf)

)

)


// replace some of an effect's key streams


(

Pbindef(\fx3,

\fx, \wah,

[\resLo, \resHi], Pwrand([[200, 300], [1500, 2000]], [0.8, 0.2], inf),

\cutOffMoveFreq, Pseq([1, 5, 20], inf),

\cleanupDelay, 0.01

)

)


q.stop;



// before playing again do Pbindef cleanup


i.do { |x| Pbindef(x).clear };




Example 8: GUI control


// control of fx params with VarGui

// control of fx sequencing by code


// SynthDefs from Ex. 1a, see also extended server resources defined there


(

// ensure we are in top envir

currentEnvironment = topEnvironment;


// pattern for fxOrder sequencing

// we want to exchange this on the fly later on


~fxs = PLshufn([1, 2, 3, [2, 3], [1, 2, 3]]);


p = PbindFx([

\instrument, \source,

\dur, PL(\dur),

\degree, PLshufn(\degree),

// Spec returns a float, so write like this

\octave, Pfunc { rrand(~octaveLo.asInteger, ~octaveHi.asInteger) },


// we want to read from code in top envir

\fxOrder, PL(\fxs, envir: topEnvironment),


\lag, Pfunc { |e| e.fxOrder.asArray.includes(2).if { 0 }{ 0.2 } },

\amp, PL(\amp),


\attackTime, PLwhite(\attackTimeLo, \attackTimeHi),

\decayTime, PLwhite(\decayTimeLo, \decayTimeHi),

\cleanupDelay, Pkey(\decayTime)

],[

\fx, \spat,

\freq, PLwhite(\spatFreqLo, \spatFreqHi),

\maxDelayTime, 0.005,

\cleanupDelay, Pkey(\maxDelayTime)

],[

\fx, \echo,

\echoDelta, PL(\echoDelta),

\decayTime, PLwhite(\echoDecayLo, \echoDecayHi),

\cleanupDelay, Pkey(\decayTime)

],[

\fx, \wah,

\resLo, PLwhite(\wahResLo, \wahResHi),

\cutOffMoveFreq, PL(\wahCutOffMoveFreq),

\mix, PL(\wahMix),

\cleanupDelay, 0.01

]

);


v = VarGui([

\degree, { |i| [0, 6, \lin, 1, i+2] }!4,

\octaveLo, [3, 6, \lin, 1, 4],

\octaveHi, [3, 6, \lin, 1, 6],

\dur, [0.2, 0.3, \lin, 0, 0.2],

\attackTimeLo, [0.01, 0.1, \lin, 0, 0.02],

\attackTimeHi, [0.01, 0.1, \lin, 0, 0.05],

\amp, [0.0, 0.5, \lin, 0, 0.2],


\decayTimeLo, [0.1, 0.5, \lin, 0, 0.2],

\decayTimeHi, [0.1, 0.5, \lin, 0, 0.4],


\spatFreqLo, [0.1, 10, \lin, 0, 0.5],

\spatFreqHi, [0.1, 10, \lin, 0, 2],


\echoDelta, [0.03, 0.1, \lin, 0, 0.05],

\echoDecayLo, [0.1, 2, \lin, 0, 0.3],

\echoDecayHi, [0.1, 2, \lin, 0, 1.8],


\wahCutOffMoveFreq, [0, 10, \lin, 0, 5],

\wahResLo, [100, 3000, \exp, 0, 200],

\wahResHi, [100, 3000, \exp, 0, 2000],

\wahMix, [0, 1, \lin, 0, 1]

], stream: p

).gui(

sliderWidth: 350,

labelWidth: 120,

varColorGroups: (0..20).clumps([12, 2, 3, 4]);

);

)


// start playing with gui and test params

// change of echoDelta necessarily causes a delay as 

// delays for events without echo have to be adapted



// change effect order sequencing


~fxs = PLseq([1, 1, 2, 2, 3, 3]);


// only spat + wah


~fxs = [1, 3];



// no effects


~fxs = 0;



// kind of polyrhythm with effects and pitches

// all 4 pitch classes are permanently reordered by PLshufn

// fixed effect sequencing


~fxs = PLseq([1, [1, 2], [1, 2, 3]]);



// stop by gui or explicitely


v.streams[0].stop;



Example 9: Using value conversions with fx data


// Effects can produce their own characteristic frequencies.

// For this it can be practical to use Event's value conversion framework.


(

// filter bank effect, level of signal very much depends on input frequencies

SynthDef(\klank, { |out, in, freq = 400, add = 7, amp = 1, ringTime = 0.1, mix = 1|

var sig, inSig = In.ar(in, 2);

sig = DynKlank.ar(`[freq * [1, add.midiratio], nil, ringTime ! 2], inSig) * amp / 100;

Out.ar(out, (1 - mix) * inSig + (sig * mix));

}).add;

)


// SynthDefs from Ex. 1a, see also extended server resources defined there


(

p = PbindFx([

\instrument, \source,

\dur, 0.2,

\amp, Pseq([0.15, 0.1, 0.1], inf),

\midinote, Pn(Pshuf([36, 36, 48, 48, 60, 65, 67])) +

Pseq([Pn(0, 40), Pn(7, 10), Pn(-5, 10)], inf) +

Pn(Pshuf([0, 0, 0, [0, 7], [0, 9], [0, 14]])),

\decayTime, Pwhite(0.8, 1.5),

\fxOrder, Pn(Pshuf([1, [1, 2], [1, 2]])),

\cleanupDelay, Pkey(\decayTime)

],[

\fx, \spat,

\freq, Prand([1, 2, 3] / 5, inf),

\maxDelayTime, 0.005,

\cleanupDelay, Pkey(\maxDelayTime)

],[

\fx, \klank,

\octave, Pwhite(5, 8),

// passing notes instead of frequencies is more pleasant here

// resulting signal is louder if pitches are near overtones of source

\note, Pn(Pshuf([0, 4, 5, 7])), 

\add, Prand([7, 12], inf),

\decayTime, 0.1,

\ringTime, Pwhite(0.1, 0.3),

\mix, 0.7,

\cleanupDelay, Pkey(\ringTime)

]

);


q = p.play;

)


q.stop;



Example 10: Parallel effects and arbitrary effect graphs


Example 10a: Parallel effects



// here source is routed to echo #1 and echo #2 in parallel,

// echo #1 (fx index 2) is a "classical" echo whereas echo #2 (fx index 3), 

// due to short echoDelta, results in an additional frequency.

// The output of echo #2 is routed to a wah-wah, echo #1 directly to out.


attachments/PbindFx/PbindFx_graph_4c.png



// SynthDefs from Ex. 1a, see also extended server resources defined there


(

p = PbindFx([

\instrument, \source,

\dur, Pseq([Pn(0.2, { rrand(8, 12) }), Pwhite(2.0, 4.0, 1)], inf),

\amp, 0.3,

\midinote, Prand([

Pwhite(80, 90, 1),

Prand([60, 67, 70, 73]) + Prand([0, -12.3, -23.7], inf),

], inf),

\fxOrder, `(0: 1, 1: [2, 3], 3: 4),

// compare with this version, where echo #1 is less present, as it also goes to wah

// \fxOrder, `(0: 1, 1: [2, 3], 3: 4, 2: 4),

\decayTime, 0.1, 

\cleanupDelay, Pkey(\decayTime)

],[

\fx, \spat,

\freq, Prand([0.1, 0.8], inf),

\maxDelayTime, 0.001,

\cleanupDelay, 0.1

],[

\fx, \echo,

\echoDelta, 0.1,

\decayTime, 3,

\cleanupDelay, Pkey(\decayTime)

],[

\fx, \echo,

\echoDelta, Pwhite(0.01, 0.05),

\decayTime, 5,

\amp, 0.5,

\cleanupDelay, Pkey(\decayTime)

],[

\fx, \wah,

\mix, 0.7,

\cutOffMoveFreq, Pseq([1, 2, 5, 10], inf),

\cleanupDelay, 0.05

]

);


q = p.play;

)


q.stop;



Example 10b: Modulation graphs


// A generalized modulating effect node has two ins: carrier and modulator.

// Fx convention of PbindFx demands one single In ugen per fx synth, but two ins  

// can simply be handled by a 2-channel In ugen and hard-panned input signals.


(

// sine source

SynthDef(\sine_adsrFixed, { |out = 0, freq = 400, decayTime = 0.5,

att = 0.005, dec = 0.01, sus = 0.2, rel = 0.3, susLevel = 0.5, amp = 0.1|

var env, sig = SinOsc.ar(freq);

env = EnvGen.kr(Env([0, 1, susLevel, susLevel, 0], [att, dec, sus, rel]), doneAction: 2);

Out.ar(out, sig ! 2 * env * amp)

}).add;


// amplitude modulation synth

SynthDef(\ampMod, { |out, in, dev = 1, amp = 1, mix = 1|

var sig, inSig = In.ar(in, 2);

sig = inSig[0] * (inSig[1] * dev + DC.ar(1)) * amp;

Out.ar(out, (1 - mix) * inSig + (sig * mix));

}).add;


// phase modulation synth

SynthDef(\phaseMod, { |out, in, maxDelay = 0.1, dev = 1, amp = 1, mix = 1|

var sig, inSig = In.ar(in, 2);

sig = DelayC.ar(inSig[0], maxDelay, maxDelay * dev * inSig[1], amp);

Out.ar(out, (1 - mix) * inSig + (sig * mix));

}).add;


// modulator synths, no Ins 


SynthDef(\sineM, { |out, in, freq = 100|

    Out.ar(out, [0, SinOsc.ar(freq)]);

}).add;


SynthDef(\pulseM, { |out, in, freq = 100, width = 0.5|

Out.ar(out, [0, Pulse.ar(freq, width, 2)]);

}).add;


SynthDef(\sawM, { |out, in, freq = 100|

Out.ar(out, [0, Saw.ar(freq)]);

}).add;

)



// blend of AM events


// spat SynthDef from Ex. 1a, see also extended server resources defined there


(

p = PbindFx([

\instrument, \sine_adsrFixed,

\dur, 1,

\susLevel, 1,

\att, 5,

\sus, 0,

\rel, 5,

\amp, 0.03,

\midinote, Pwhite(40, 80),

\fxOrder, `(0: 1, 3: 1, 1: 2),


\decayTime, 1,

\cleanupDelay, 12

],[

\fx, \ampMod,

\dev, Pwhite(0.1, 0.6)

],[

\fx, \spat,

\freq, Pwhite(0.2, 2),

\maxDelayTime, 0.005,

\cleanupDelay, Pkey(\maxDelayTime)

],[

\fx, \pulseM,

\freq, Pwhite(200, 1000)

]

);


q = p.play;

)


q.stop;



Example 10c: Modulation graphs, changed per event


// fx graphs corresponding to fxOrder `(0:1, 4:1, 1:6) and `(0:1, 5:1, 1:6), src = \sine_adsrFixed:


attachments/PbindFx/PbindFx_graph_4a.png

// fx graph corresponding to fxOrder `(0:2, 3:2, 2:6), src = \sine_adsrFixed:

attachments/PbindFx/PbindFx_graph_4b.png


// SynthDefs from Ex. 10b

// spat SynthDefs from Ex. 1a, see also extended server resources defined there


(

p = PbindFx([

        \instrument, \sine_adsrFixed,

        \amp, 0.01,

\dur, 0.3,

        \susLevel, 1,

        \att, 0.01,

        \sus, 0.15,

\rel, Pwhite(0.3, 1.2),

        \amp, 0.05,


        \midinote, Pwhite(30, 60) + Prand([0, [0, -12.5]], 200),


// changes between amplitude (pulse and saw) and phase modulation (sine)


        \fxOrder, Pn(Pshuf([

                `(0:1, 4:1, 1:6),

                `(0:1, 5:1, 1:6),

                `(0:2, 3:2, 2:6)

            ])),


        // equivalent:

        // the source stream returns pairs, where the first number indicates

        // the modulation type and the second number the modulator,

        // the collect function packs the data into the right format of a ref'd Event.


        // \fxOrder, Pn(Pshuf([ [1, 4], [1, 5], [2, 3] ]))

        // .collect { |x| ().putPairs([0, x[0], x[1], x[0], x[0], 6]).asRef },


        \decayTime, 2,

        \cleanupDelay, Pkey(\decayTime)

    ],[

        \fx, \ampMod,

        \dev, Pwhite(0.1, 0.5)

    ],[

        \fx, \phaseMod,

        \dev, Pwhite(0.03, 0.05)

    ],[

        \fx, \sineM,

        \freq, Pwhite(150, 700)

    ],[

        \fx, \sawM,

        \freq, Pwhite(150, 700)

    ],[

        \fx, \pulseM,

        \freq, Pwhite(150, 700)

    ],[

        \fx, \spat,

        \freq, Pwhite(0.1, 1),

        \maxDelayTime, 0.005,

        \cleanupDelay, Pkey(\maxDelayTime)

    ]

);


q = p.play;

)


q.stop;