DXEnvFan returns crossfade envelopes according to demand-rate control


Part of: miSCellaneous


Inherits from: AbstractDX


DXEnvFan returns the multichannel envelope signal, which is used by DXMix / DXMixIn / DXFan / DXFanOut implicitely. It can be used as envelope and trigger at the same time, which leads to applications such as crossfading PlayBufs and different kinds of granulation, using a buffer or not. 


NOTE: As interface and conventions of DX ugens are nearly identical, I didn't double examples for all features. It's recommended to start with the DX suite overview and go through the help file examples in this order: DXMix - DXMixIn - DXEnvFan - DXEnvFanOut - DXFan - DXFanOut. Some general conventions are treated in detail in the following examples: fades and steps in DXMix help, Ex.2 – width and offset arguments in DXMix help, Ex.3 – multichannel expansion in DXMix help, Ex.6 – crossfade types in DXEnvFan help, Ex.1.


NOTE: PanAz.ar's args pos and orientation were scaled wrongly in SC versions up to 3.8. DX ugens neutralize this bug by inverse scaling, so it should actually work the same with SC versions before 3.9 with the exception of examples with modulatable width (disabled in earlier versions). I didn't encounter differences in any other test examples, however I'd rather recommend a SC version from 3.9 onwards, if you have the choice.


NOTE: Depending on the multichannel sizes it might be necessary to increase server resources, i.e. the number of interconnect buffers (e.g. s.options.numWireBufs = 256; s.reboot). See Ex.8 from DXMix help and Ex.2, Ex.4 from DXEnvFan help for aspects of CPU demand.


NOTE: In my tests timing was exact up to one sample. So when used for granulation DX ugens avoid the inevitable inccuracies of language-based triggering in realtime. However care has to be taken: fade and step times must be larger than the duration of a control cycle. With default values sampleRate = 44100 Hz and blockSize = 64, this equals ca. 0.00145 sec. If you go below, the fade mechanism is messed up and you get jumps and clicks. Accordingly with fadeModes 3 and 4 you have to ensure that the remaining 'real' stepTime, which is calculated by stepTime minus fadeTime, is larger than this threshold. But as a workaround you can always lower the blocksize. See DXFan help Ex.4 for aspects of granulation with high trigger rates / short grain durations.


NOTE: The current implementation is bound to counting with Dseries and – inherent to 32 bit floats – the integer accuracy limit of 2 ** 24 - 1 = 16777215. This can be an issue with setups that are using extreme short durations for hours.


CREDITS: Thanks to Wouter Snoei for his PlayBufCF class. It gave me a lot of inspiration for DX ugens – although in the end the implementation with PanAz and DemandEnvGen is quite different. Thanks also to Till Bovermann for ironing out a longstanding bug in PanAz.



See also: DX suite, DXMix, DXMixIn, DXEnvFanOut, DXFan, DXFanOut, Buffer Granulation, Live Granulation, PbindFx, kitchen studies



Creation / Class Methods


*ar (out, fadeTime = 1, stepTime = 1, fadeMode = 0, sine = 1, equalPower = 1, power = 1, curve = 0, allowTypeSeq = 0, fadeRate = \ar, maxFadeNum = inf, maxWidth = 2, width = 2, initOutOffset = 0, maxDynOutOffset = 1, dynOutOffset = 0, allowFadeEnd = 1, size = 2, bus = nil, zeroThr = nil, doneAction = 0)

out - Determines the sequence of channels between which the signal should be crossfaded.

An channel index, a demand rate or other ugen returning channel indices or 

a SequenceableCollection of such, causing multichannel expansion.

If in this case the overall multichannel size is larger than the size of out

and the latter contains demand rate ugens, they must all be wrapped into Functions.

fadeTime - A fade time, a demand rate or other ugens returning fade times or 

a SequenceableCollection of such, causing multichannel expansion.

If in this case the overall multichannel size is larger than the size of fadeTime

and the latter contains demand rate ugens, they must all be wrapped into Functions.

The interpretation of fadeTime depends on fadeMode.

fadeTime must be larger than the duration of a control cycle.

Defaults to 1.

stepTime - A step time, a demand rate or other ugens returning step times or 

a SequenceableCollection of such, causing multichannel expansion.

If in this case the overall multichannel size is larger than the size of stepTime

and the latter contains demand rate ugens, they must all be wrapped into Functions.

The interpretation of stepTime depends on fadeMode.

stepTime must be larger than the duration of a control cycle.

Defaults to 1.

fadeMode - Integers between 0 and 4 or

a SequenceableCollection of such, causing multichannel expansion. Not modulatable.

fadeMode = 0: only fadeTimes are used, no steps

fadeMode = 1: alternate steps and fades, begin with step; stepTime means time without fade

fadeMode = 2: alternate fades and steps, begin with fade; stepTime means time without fade

fadeMode = 3: alternate steps and fades, begin with step; stepTime means sum of step and fade,

thus stepTime must be larger than fadeTime,

the difference must be larger than the duration of a control cycle

fadeMode = 4: alternate fades and steps, begin with fade; stepTime means sum of fade and step,

thus stepTime must be larger than fadeTime,

the difference must be larger than the duration of a control cycle

Defaults to 0.

sine - Determines the crossfade type: sine-based or not.

A Boolean, 0 or 1 or a demand rate or other ugen returning sine numbers or 

a SequenceableCollection of such, causing multichannel expansion.

If in this case the overall multichannel size is larger than the size of sine

and the latter contains demand rate ugens, they must all be wrapped into Functions.

Modulating this arg is only possible if allowTypeSeq equals 1.

Defaults to 1. 

equalPower - Determines if crossfading of equal power type (square root) should be applied.

A Boolean, 0 or 1 or a demand rate or other ugen returning equalPower numbers or 

a SequenceableCollection of such, causing multichannel expansion.

If in this case the overall multichannel size is larger than the size of equalPower

and the latter contains demand rate ugens, they must all be wrapped into Functions.

Modulating this arg is only possible if allowTypeSeq equals 1.

Defaults to 1. 

power - This only comes into play if equalPower equals 0, then it's applied to the 

crossfade amplitude. If power and curve are passed, power applies before.

A positive Number or a demand rate or other ugen returning positive power numbers or 

a SequenceableCollection of such, causing multichannel expansion.

If in this case the overall multichannel size is larger than the size of power

and the latter contains demand rate ugens, they must all be wrapped into Functions.

Sequencing this arg with demand rate ugens is only possible if allowTypeSeq equals 1.

Defaults to 1. 

curve - This only comes into play if equalPower equals 0, then it's applied to the 

crossfade amplitude according to the lincurve mapping. 

If power and curve are passed, power applies before.

A Number or a demand rate or other ugen returning curve numbers or 

a SequenceableCollection of such, causing multichannel expansion.

If in this case the overall multichannel size is larger than the size of curve

and the latter contains demand rate ugens, they must all be wrapped into Functions.

Sequencing this arg with demand rate ugens is only possible if allowTypeSeq equals 1.

Calculation of curvature is not giving reliable results when width and / or dynOutOffset are 

being modulated at the same time.

Defaults to 0. 

allowTypeSeq - Enables sequencing of sine, equalPower, power and curve with 

demand rate ugens and modulating of sine and equalPower with other ugens.

A Boolean, 0 or 1 or a SequenceableCollection of such, causing multichannel expansion.

Not modulatable. As this requires more ugens running in parallel it is disabled by default = 0.

fadeRate - One of the Symbols \ar and \kr, determining the crossfade rate used by PanAz or

a SequenceableCollection of such, causing multichannel expansion. Not modulatable.

Defaults to \ar.

maxFadeNum - Integer determining the maximum number of fades, after which doneAction applies. 

A SequenceableCollection causes multichannel expansion. Not modulatable.

Defaults to inf.

maxWidth - An Integer determining the maximum width or

a SequenceableCollection of such, causing multichannel expansion, width goes into PanAz's width arg.

maxWidth increases the internally used and potentially needed number of parallel channels. Not modulatable.

Determines the size of the returned multichannel signal.

Defaults to 2.

width - Integer, Float, UGen (only from SC 3.9 onwards) or a SequenceableCollection of such, 

causing multichannel expansion. Not modulatable in versions earlier than SC 3.9.

It determines the width according to PanAz's width arg. Note that a ugen's output must not exceed maxWidth.

In case of  DXEnvFan's use as trigger you might want to set it really smaller than maxWidth.

Defaults to 2.

initOutOffset - An Integer or Float or a SequenceableCollection of such, causing multichannel expansion.

Determines an initial offset for PanAz's pos arg.

This can be useful for a start with full or reduced width, see examples. 

Not modulatable. Defaults to 0.

maxDynOutOffset - An Integer or Float or a SequenceableCollection of such, causing multichannel expansion.

Determines the maximum dynOutOffset to be expected.

maxDynOutOffset increases the internally used and potentially needed number of parallel channels. 

Not modulatable. Defaults to 1.

dynOutOffset - UGen, Integer or Float or

a SequenceableCollection of such, causing multichannel expansion.

By passing a ugen the movement between channels can be modulated.

Note that a ugen's output must not exceed maxDynOutOffset.

Defaults to 0.

allowFadeEnd - Integer, Boolean or a SequenceableCollection of such, causing multichannel expansion.

Determines if a demand rate input to out with finite length will be monitored, which needs a quite complicated 

trigger logic and more running ugens. If set to 0, the behaviour after the end of out is undefined.

Defaults to 1.

size - Integer or a SequenceableCollection of such, causing multichannel expansion. 

Determines the size of the returned multichannel envelope signal.

Not modulatable. Defaults to 2.

bus - Bus, bus index or a SequenceableCollection of such, causing multichannel expansion. 

Determines whether a private multichannel bus should be used for channel switching.

This is recommended for larger width sizes (> 10 or so) as otherwise the number of ugens 

might result in an overflow error.

Not modulatable. Defaults to nil.

zeroThr - A Number or a ugen returning zeroThr numbers or 

a SequenceableCollection of such, causing multichannel expansion.

Determines if output values below this threshold are replaced by 0.

This makes sense if the output signal is used as trigger (e.g. with DXEnvFan).

In the case of low power numbers small inaccuracies are amplified, this is avoided

with an appropriate zeroThr (e.g. = 0.001), as the operation is applied before taking the power.

As this requires more ugens running in parallel it is disabled by default = nil.

doneAction - Integer or a SequenceableCollection of such, causing multichannel expansion.

Determines the doneAction after maxFadeNum is exceeded.

Defaults to 0.


*kr (out, fadeTime = 1, stepTime = 1, fadeMode = 0, sine = 1, equalPower = 1, power = 1, curve = 0, allowTypeSeq = 0, fadeRate = \ar, maxFadeNum = inf, maxWidth = 2, width = 2, initOutOffset = 0, maxDynOutOffset = 1, dynOutOffset = 0, allowFadeEnd = 1, size = 2, bus = nil, zeroThr = nil, doneAction = 0)



Examples


(

// load with extended resources

s = Server.local;

Server.default = s;

s.options.numWireBufs = 256; 

s.reboot;

)


Ex.1) Basic types of crossfades


// For normal crossfades the default values - equal power panning of sine type - should be fine,

// other types of crossfade envelopes can be taken for other purposes such as granular synthesis.



// default width 2, default equal power crossfading


// employs crossfading from PanAz, which results in envelopes of sine type, from which

// the square root is taken


(

{

DXEnvFan.ar(

Dseq([3,2,1,4,5,6,7,0], inf),

size: 8,

fadeTime: 0.01,

// fadeMode: 1,

// stepTime: 0.015

)

}.plot(0.1)

)



// when dropping equalPower we get the pure sine envelope

// higher width


(

{

DXEnvFan.ar(

Dseq([3,2,1,4,5,6,7,0], inf),

size: 8,

fadeTime: 0.01,

maxWidth: 8,

width: 5.5,

equalPower: 0,

// fadeMode: 1,

// stepTime: 0.015

)

}.plot(0.1)

)



// type sine = 0 switches to a linear curve base,

// with default equalPower = 1 this leads to a square root envelope



(

{

DXEnvFan.ar(

Dseq([3,2,1,4,5,6,7,0], inf),

size: 8,

fadeTime: 0.01,

sine: 0,

// fadeMode: 1,

// stepTime: 0.015

)

}.plot(0.1)

)



// a linear curve base without equalPower means a linear crossfade


(

{

DXEnvFan.ar(

Dseq([3,2,1,4,5,6,7,0], inf),

size: 8,

fadeTime: 0.01,

sine: 0,

equalPower: 0,

// fadeMode: 1,

// stepTime: 0.015

)

}.plot(0.1)

)



// if equalPower is set to 0 you can choose a power

// check alternative values for sine and power also


(

{

DXEnvFan.ar(

Dseq([3,2,1,4,5,6,7,0], inf),

size: 8,

fadeTime: 0.01,

equalPower: 0,

sine: 0,  // 1

power: 3, // 0.3

// fadeMode: 1,

// stepTime: 0.015

)

}.plot(0.1)

)



// power can be a ugen too


(

{

DXEnvFan.ar(

Dseq([3,2,1,4,5,6,7,0], inf),

size: 8,

fadeTime: 0.01,

equalPower: 0,

sine: 1,  // 1

power: SinOsc.ar(10, -pi/2).range(0.5, 10),

// fadeMode: 1,

// stepTime: 0.015

)

}.plot(0.1)

)



// if equalPower is set to 0 you can also choose a curve arg

// which is passed as curvature to lincurve


(

{

DXEnvFan.ar(

Dseq([3,2,1,4,5,6,7,0], inf),

size: 8,

fadeTime: 0.01,

equalPower: 0,

curve: 3,  // -3

sine: 0,

// fadeMode: 1,

// stepTime: 0.015

)

}.plot(0.1)

)



// curve can be a ugen too


(

{

DXEnvFan.ar(

Dseq([3,2,1,4,5,6,7,0], inf),

size: 8,

fadeTime: 0.01,

curve: Line.ar(5, -5, 0.1),  // -3

sine: 0,

equalPower: 0,

// fadeMode: 1,

// stepTime: 0.015

)

}.plot(0.1)

)



// Passing a dynOutOffset arg - wobbled sliding


(

{

DXEnvFan.ar(

Dseq([3,2,1,4,5,6,7,0], inf),

size: 8,

fadeTime: 0.01,

dynOutOffset: SinOsc.ar(150).range(0, 1),

// fadeMode: 1,

// stepTime: 0.015

)

}.plot(0.1)

)



// This is not recommended:

// With dynOutOffset or modulated width a curve arg leads to irregularities


(

{

DXEnvFan.ar(

Dseq([3,2,1,4,5,6,7,0], inf),

size: 8,

fadeTime: 0.01,

dynOutOffset: SinOsc.ar(150).range(0, 1),

equalPower: 0,

curve: 1,

// fadeMode: 1,

// stepTime: 0.015

)

}.plot(0.1)

)



// out can also be a ugen, but note that with equalPower (default)

// blending the same channel leads to amplitude pikes,

// to avoid that take a linear crossfade


(

{

DXEnvFan.ar(

LFDNoise3.ar(15).range(0, 7),

size: 8,

fadeTime: 0.01,

stepTime: 0.02,

fadeMode: 1,

sine: 0, 

equalPower: 1  // compare 0

)

}.plot(1)

)



Ex.2) Sequencing crossfade types and parameters


// sequencing crossfade types and parameters must be allowed explicitely as 

// it is more CPU-costy


// alternate linear and pure sine

(

{

DXEnvFan.ar(

Dseq([3,2,1,4,5,6,7,0], inf),

size: 8,

fadeTime: 0.01,

allowTypeSeq: 1,

sine: Dseq([0, 1], inf),

equalPower: 0, // Dseq([0, 1], inf)

// fadeMode: 1,

// stepTime: 0.015

)

}.plot(0.1)

)



// curvature sequencing


(

{

DXEnvFan.ar(

Dseq([3,2,1,4,5,6,7,0], inf),

size: 8,

fadeTime: 0.01,

allowTypeSeq: 1,

sine: 0,

equalPower: 0, 

curve: Dseq([-3, -3, 3, 3], inf),

// fadeMode: 1,

// stepTime: 0.015

)

}.plot(0.1)

)



Ex.3) The 'zeroThr' arg


// There might occur small inaccuracies in the calculation of crossfade values.

// This is irrelevant in most cases, but if you use the output signal as trigger

// it is definitely unwanted.

// You can supress such if you set an appropriate zero threshold.



// The effect of unwanted envelope segments especially becomes apparent

// with small power values that blow up values near 0.

// Values below the zeroThr are set to 0 before applying power or curvature.


// Compare version without passing zeroThr.


(

{

DXEnvFan.ar(

Dseq([3,2,1,4,5,6,7,0], inf),

size: 8,

fadeTime: 0.01,

equalPower: 0,

sine: 0,

power: 0.3,

fadeMode: 1,

stepTime: 0.015,

zeroThr: 0.002

)

}.plot(0.3)

)




Ex.4) The 'bus' arg


// As channel switching with DXEnvFan / DXFan is implemented by "watcher ugens",

// it becomes costy if the number of output channels increases (growth of quadratic order).

// The effort is lowered significantly if you pass a reserved bus for this or use DXEnvFanOut,

// this also concerns DXFan / DXFanOut.


(

// load with extended resources

s = Server.local;

Server.default = s;

s.options.numPrivateAudioBusChannels = 256;

s.options.memSize = 8192 * 4;

s.options.numWireBufs = 512;

s.reboot;

)



// on my machine this example needs ca. 2.5 % CPU (818 ugens) ...


(

x = {

    DXEnvFan.ar(

        Dshuf((0..29), inf),

        size: 30,

        fadeTime: 0.005

    ) * 0.25

}.play

)


x.release


// ... whereas this needs ca. 0.6 % CPU (167 ugens)


(

a = Bus.audio(s, 30);


x = {

    DXEnvFan.ar(

        Dshuf((0..29), inf),

        size: 30,

        fadeTime: 0.005,

        bus: a

    ) * 0.25

}.play

)


x.release


(

a.free;

b.free;

)




// care has to be taken with buses and multichannel expansion:


// here two buses have to passed as otherwise we get a wrong result,

// the same bus would be taken for different calculations at the same time


(

a = Bus.audio(s, 8);

b = Bus.audio(s, 8);


{

Mix(DXEnvFan.ar(

[Dseq((0..7), inf), Dseq((7..0), inf)],

size: 8,

fadeTime: 0.01,

equalPower: 0,

bus: [a, b]

))

}.plot(0.1)

)


(

a.free;

b.free;

)




Ex.5) PlayBuf crossfading


// Here a two channel envelope signal is used to crossfade between

// two channels of a PlayBuf, as rates are close we get a trill effect.


// Note that the envelope signal is used three times:


// as a trigger to the startPos within PlayBuf

// as a trigger for the demand ugen that determines the startPos within PlayBuf

// as an envelope for the PlayBuf


b = Buffer.read(s, Platform.miSCellaneousDirs[0] +/+ "Sounds" +/+ "kitchen_sounds_1.wav");


(

x = {

var sig, env = DXEnvFan.ar(

Dseq([0, 1], inf),

fadeMode: 3,

stepTime: 0.05,

fadeTime: 0.01,

// to ensure right triggering set zeroThr

zeroThr: 0.002

);


sig = PlayBuf.ar(

1,

b,

[1, 1.1] * BufRateScale.kr(b),

env,

Demand.ar(

env,

0,

Dstutter(

5,

Dwhite(0.1, 0.9)

) * BufFrames.ir(b)

)

) * 1.2 * env;

// do a bit correlation

Splay.ar(sig, 0.8)

}.play

)


x.release



// L and R get different mixes,

// both take the same envelope trigger which switches

// between PlayBufs of different rates, the difference between L and R

// comes from different random start positions.


(

x = {

var buf, env = DXEnvFan.ar(

Dseq([Dshuf((0..4)), Dshuf((5..7))], inf),

size: 8,

fadeMode: 3,

stepTime: 0.2,

fadeTime: 0.02,

zeroThr: 0.002

);


{ 

Mix(PlayBuf.ar(

1,

b,

{ |j| j / 4 + 0.3 } ! 8 * BufRateScale.kr(b),

env,

Demand.ar(env, 0, Drand((2..5)/10, inf) * BufFrames.ir(b)),

loop: 0,

) * env) 

} ! 2 ;

}.play

)



x.release


// variant with multichannel expansion

// two 8 ch trigger envelopes are used for L and R mixes

(

x = {

var buf, env = DXEnvFan.ar(

[Dseq((0..7), inf), Diwhite(0, 7)],

size: 8,

fadeMode: 3,

stepTime: 0.2,

fadeTime: 0.02,

zeroThr: 0.002

);


{ |i| Mix(PlayBuf.ar(

1,

b,

{ |j| j / 4 + 0.3 } ! 8 * BufRateScale.kr(b),

env[i],

// same positions go parallel

Demand.ar(env[i], 0, Dstutter(8, Dseq((2..5)/10, inf)) * BufFrames.ir(b)),

loop: 0,

) * env[i]) } ! 2 ;

}.play

)


x.release




Ex.6) Granulation, sequencing of fxs and fx parameters per grain



b = Buffer.read(s, Platform.miSCellaneousDirs[0] +/+ "Sounds" +/+ "kitchen_sounds_1.wav");


// sequencing of band pass frequency

// see also Buffer Granulation tutorial, Ex. 1f


(

a = Bus.audio(s, 8);


x = {

    var sig, env = DXEnvFan.ar(

        Dseq((0..7), inf),

        size: 8,

maxWidth: 8,

width: 5,

        fadeTime: 0.01,

        // to ensure right triggering set zeroThr

        zeroThr: 0.002,

        bus: a

    );


    sig = PlayBuf.ar(

        1,

        b,

        BufRateScale.kr(b),

        env,

        Demand.ar(env, 0, Dbrown(0.2, 0.5, 0.0025) * BufFrames.ir(b)),

        loop: 0,

    ) * env;

    

    // multichannel trigger polls from single sequencing source

// ensure non-zero filter freq for later triggers with max

sig = BPF.ar(sig, Demand.ar(env, 0, Dbrown(200, 2000, 100)).max(200), 0.1);


    // do a bit correlation

    Splay.ar(sig, 0.6) * 7

}.play

)


x.release


a.free



// sequencing of different fxs


(

a = Bus.audio(s, 8);


x = {

    var sig, env = DXEnvFan.ar(

        Drand((0..7), inf),

        size: 8,        

        maxWidth: 8,

        // oscillation of overlap

        width: LFDNoise3.ar(1).range(2, 6),

        fadeTime: 0.015,

        // to ensure right triggering set zeroThr

        zeroThr: 0.002,

        bus: a

    );


    sig = PlayBuf.ar(

        1,

        b,

        BufRateScale.kr(b),

        env,

        Demand.ar(env, 0, Dbrown(0.2, 0.5, 0.005) * BufFrames.ir(b)),

        loop: 0,

    ) * env;


    // selection of fxs as well as fx params triggered with envelope

    // ring modulation, bit crusher and band pass

    sig = Select.ar(Demand.ar(env, 0, Dstutter(Diwhite(5, 30), Dxrand([0, 1, 2], inf))), [

        sig * SinOsc.ar(Demand.ar(env, 0, Dbrown(250, 1000, 200))) * 1.5,

        sig.round(2 ** Demand.ar(env, 0, Dbrown(-1, -4, 1))).lag(0.005) * 1.5,

// ensure non-zero filter freq for later triggers with max

BPF.ar(sig, Demand.ar(env, 0, Dbrown(200, 2000, 200)).max(200), 0.2) * 5

    ]);


    // do a bit correlation

    Splay.ar(sig, 0.6)

}.play

)


x.release


a.free

 


Ex.7) Multichannel expansion



// Not as complicated options as with DXMix

// we get an array of multichannel envelopes, here a mix of them


(

{

Mix(DXEnvFan.ar(

(0..3).collect { |i| Dseq((0..3).rotate(i), inf) },

fadeTime: [0.01, 0.05],

size: 4,

width: 1

))

}.plot(0.2)

)


// see Ex.4 for a delicate case with bus args