TZeroXBufRd triggers sequences of segments between zero crossings from one or more buffers with demand-rate control


Part of: miSCellaneous


Inherits from: UGen


TZeroXBufRd is for triggering possibly overlapped segments between zero crossings (half wavesets) from one or more buffers, whereby several reading and processing parameters can be sequenced with demand rate ugens. Full waveset sequences can so be generated as a special case. It needs analysis data prepared with ZeroXBufWr. For consecutive reading of segments between zero crossings see ZeroXBufRd. ZeroXBufRd / TZeroXBufRd can be used for a number of synthesis / processing techniques in a field between wavesets [1, 4, 5], pulsar synthesis [1, 3], buffer modulation and rectification (which are both a kind of waveshaping) and stochastic concatenation methods [2, 6]. There are already existing SC waveset implementations like Alberto de Campo's Wavesets quark (https://github.com/supercollider-quarks/quarks) and Olaf Hochherz's SPList (https://github.com/olafklingt/SPList), which do language-side analysis and Fabian Seidl's RTWaveSets plugin (https://github.com/tai-studio/RTWaveSets). My focus has been server-side analysis and demand rate ugen control of half waveset parameters as well as multichannel and buffer switch options. Realtime control while analysis is possible, as long as reading is only refering to already analysed sections, but clearly most flexibility is given with a fully analysed buffer, which can also be done in quasi realtime.


NOTE: Depending on the multichannel sizes and the options used (rate and dir sequencing) it might be necessary to increase server resources, i.e. the number of interconnect buffers and / or memory size (e.g. s.options.numWireBufs = 256; s.options.memSize = 8192 * 32; s.reboot). Because of overlappings this is more relevant with TZeroXBufRd than with ZeroXBufRd.


NOTE: Often it pays to adjust zero crossings in the sound buffer effectively to 0, that way sawtooth-like interpolation artefacts can be avoided. See Ex.1 below.


NOTE: Demand rate UGens in ZeroXBufRd / TZeroXBufRd must always use inf as repeats arg, this is of course not necessary for nested ones. You might pass a length arg though (Ex. 5).


NOTE: The distance between triggers must remain above control duration, otherwise the synthesis fails. For faster trigerring you'd have to use the server with a lower blocksize.


NOTE: As triggered half wavesets can overlap you'd have to care for a sufficiently large 'overlapSize' arg. See Ex.3 for possible estimations.


NOTE: For avoiding too long half wavesets it might be useful to apply LeakDC resp. a high pass filter before analysis.


NOTE: In rare cases I noticed corrupted buffers in multi buffer examples for no obvious reason.



CREDITS: Thanks to Tommaso Settimi for an inspiring discussion, which gave me a nudge to tackle these classes. 



REFERENCES:


[1] de Campo, Alberto. "Microsound" In: Wilson, S., Cottle, D. and Collins, N. (eds). 2011. 

The SuperCollider Book. Cambridge, MA: MIT Press, 463-504.

[2] Luque, Sergio (2006). Stochastic Synthesis, Origins and Extensions. Institute of Sonology, Royal Conservatory, The Netherlands. 

http://sergioluque.com


[3] Roads, Curtis (2001). Microsound. Cambridge, MA: MIT Press.

[4] Seidl, Fabian (2016). Granularsynthese mit Wavesets für Live-Anwendungen. Master Thesis, TU Berlin.

https://www2.ak.tu-berlin.de/~akgroup/ak_pub/abschlussarbeiten/2016/Seidl_MasA.pdf


[5] Wishart, Trevor (1994). Audible Design. York: Orpheus The Pantomime Ltd. 


[6] Xenakis, Iannis (1992). Formalized Music. Hillsdale, NY: Pendragon Press, 2nd Revised edition.




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



Creation / Class Methods


*ar (sndBuf, zeroXBuf, bufMix, trig, zeroX = 0, xNum = 1, xRep = 1,  power = 1, mul = 1, add = 0, rate = 1, dir = 1, 

interpl = 4, overlapSize = 10, length = inf, maxTime = inf, att = 0, rel = 1, curve = -4, doneAction = 0)

sndBuf - Buffer or SequenceableCollection of Buffers to read the data from, data must correspond to zeroXBuf.

zeroXBuf - Analysis Buffer resp. SequenceableCollection of such, prepared with ZeroXBufWr. 

Must refer to data passed to sndBuf.

bufMix - A Number indicating the sndBuf index, a demand rate or other ugens returning sndBuf indices or 

a SequenceableCollection of such.

If bufMix equals nil (default) the size of the returned signal equals the size of sndBuf,

otherwise it equals its own size.

trig - A trigger signal for starting new half waveset groups.

zeroX - A Number indicating the index in zeroXBuf, a demand rate or other ugens returning zeroXBuf indices or 

a SequenceableCollection of such.

If in this case the overall multichannel size determined by sndBuf or bufMix is larger than the size of zeroX

and the latter contains demand rate ugens, they must all be wrapped into Functions for being used more than once.

Defaults to 0.

xNum - Determining the number of half wavesets starting at zeroX, a demand rate or other ugens returning these numbers or 

a SequenceableCollection of such.

If in this case the overall multichannel size determined by sndBuf or bufMix is larger than the size of xNum

and the latter contains demand rate ugens, they must all be wrapped into Functions for being used more than once.

Defaults to 1.

xRep - Determining the number of repetitions of half wavesets resp. half waveset groups (given by xNum) starting at zeroX, 

a demand rate or other ugens returning these numbers or a SequenceableCollection of such.

If in this case the overall multichannel size determined by sndBuf or bufMix is larger than the size of xNum

and the latter contains demand rate ugens, they must all be wrapped into Functions for being used more than once.

Defaults to 1.

power - Used for processing the buffer signal according to the formula: sig ** power * mul + add per half waveset group. 

Must be a positive Number, a demand rate or other ugens returning power values or 

a SequenceableCollection of such.

If in this case the overall multichannel size determined by sndBuf or bufMix is larger than the size of power

and the latter contains demand rate ugens, they must all be wrapped into Functions for being used more than once.

Defaults to 1.

mul - Used for processing the buffer signal according to the formula: sig ** power * mul + add per half waveset group. 

Must be a Number, a demand rate or other ugens returning mul values or 

a SequenceableCollection of such.

If in this case the overall multichannel size determined by sndBuf or bufMix is larger than the size of mul

and the latter contains demand rate ugens, they must all be wrapped into Functions for being used more than once.

Defaults to 1. 

add - Used for processing the buffer signal according to the formula: sig ** power * mul + add per half waveset group. 

Must be a Number, a demand rate or other ugens returning add values or 

a SequenceableCollection of such.

If in this case the overall multichannel size determined by sndBuf or bufMix is larger than the size of add

and the latter contains demand rate ugens, they must all be wrapped into Functions for being used more than once.

Defaults to 0. 

rate - Determines the playback rate per half waveset group. 

Must be a positive Number, a demand rate or other ugens returning rate values or a SequenceableCollection of such.

If in this case the overall multichannel size determined by sndBuf or bufMix is larger than the size of rate

and the latter contains demand rate ugens, they must all be wrapped into Functions for being used more than once.

Defaults to 1.

dir - Determines the playback direction of half waveset groups. 

Must be +1 or -1, a demand rate or other ugens returning dir values or a SequenceableCollection of such.

If in this case the overall multichannel size determined by sndBuf or bufMix is larger than the size of dir

and the latter contains demand rate ugens, they must all be wrapped into Functions for being used more than once.

Defaults to 1. 

interpl - Determines the interpolation type for the BufRd ugens. 

Must equal 1 (no), 2 (linear) or 4 (cubic) or a SequenceableCollection of these numbers.

Defaults to 4. 

overlapSize - Determines the maximum overlap of half waveset groups. 

This is a fixed number or a SequenceableCollection thereof determining the size of internally used

multichannel signals for overlappings. If this number is too low the synthesis fails.

See Ex. 3 for estimating a sufficiently large value.

Defaults to 10. 

length - Determines the number of triggers before release of the overall asr envelope. 

Can be a Sequenceable Collection too. Overruled by maxTime if this is reached before.

Defaults to inf. 

maxTime - Determines the time before release of the overall asr envelope. 

Can be a Sequenceable Collection too. Overruled by length if this is reached before.

Defaults to inf. 

att - Attack time of overall asr envelope or SequenceableCollection thereof. 

Defaults to 0. 

rel - Release time of overall asr envelope or SequenceableCollection thereof.  

Defaults to 0. 

curve - Curve of overall asr envelope or SequenceableCollection thereof.  

Defaults to -4. 

doneAction - Done action of overall asr envelope or SequenceableCollection thereof.  

Defaults to 0. 


Examples


// See also the examples of ZeroXBufRd help. Most features work in the same way,

// this help file focusses rather on the differences.



Ex.1) Basic usage

// prepare two short buffers for audio and zero crossing data

// the size of needed zeroX data space can be roughly estimated


(

b = Buffer.alloc(s, 256);

z = Buffer.alloc(s, 32);

)


// analyse a short snippet of modulation


(

x = {

var src = SinOsc.ar(SinOsc.ar(1200, 0, 1000, 100)) * SinOsc.ar(700) - 0.1;

ZeroXBufWr.ar(src, b, z, startWithZeroX: 0, doneAction: 2);

Silent.ar

}.play

)



// check the waveform


b.plot




// It's important to note that the maximum trigger rate should be below control rate,

// otherwise sequencing with demand rate ugens is messed up.

// This limitation can be circumvented by rebooting the server with a lower blockSize.


// demand rate ugens in TZeroXBufRd should always use inf as repeats arg


(

~maxTrigRate = s.sampleRate / s.options.blockSize;


x = {

TZeroXBufRd.ar(

b, z,

trig: Impulse.ar(MouseX.kr(10, ~maxTrigRate)),

zeroX: Dseq([2, 3], inf),

rate: 0.2

) * 0.1

}.play

)


s.scope


x.release


// In this example waveforms are slightly crossing the x axis.

// This comes from the fact that buffer values at zero crossing positions are not equal zero,

// but just of a different sign than the sample before.

// The effect is also explained in ZeroXBufRd's help Ex.7.


(

{

TZeroXBufRd.ar(

b, z,

trig: Impulse.ar(150),

zeroX: Dseq([2, 3], inf),

rate: 0.2

) * 0.2

}.plot(0.03)

)



// To get cleaner waveforms it's therefore recommended to adjust zero crossings in the buffer.

// This can also be done while analysis by using the flag 'adjustZeroXs' in ZeroXBufWr.


b.adjustZeroXs(z)



// smoother result


(

{

TZeroXBufRd.ar(

b, z,

trig: Impulse.ar(150),

zeroX: Dseq([2, 3], inf),

rate: 0.2

) * 0.2

}.plot(0.03)

)



// mul and dir can be used in the same way as with ZeroXBufRd


(

x = {

TZeroXBufRd.ar(

b, z,

trig: Impulse.ar(MouseX.kr(30, ~maxTrigRate)),

zeroX: Dseq([2, 3], inf) + LFDNoise0.ar(10).range(0, 5),

mul: Dseq([0.05, 0.1, 0.35], inf),

rate: 0.5,

dir: Dseq([1, -1], inf),

)

}.play

)


x.release



// unlike with ZeroXBufRd with rate there's no restriction for using 

// combinations of demand rate and normal ugens


(

x = {

TZeroXBufRd.ar(

b, z,

trig: Impulse.ar(MouseX.kr(30, ~maxTrigRate)),

zeroX: Dseq([2, 3], inf),

mul: Dseq([0.05, 0.1, 0.35], inf),

rate: Dseq([0.2, 0.5, 1], inf) * SinOsc.ar(5).range(0.8, 1.2),

dir: Dseq([1, -1], inf),

)

}.play

)


x.release



// power arg

// be careful with this arg moving away from 1 ! 

// high power values can result in loud signals if the source has values outside [-1, 1] and

// small power values can also become loud with source values near zero


(

x = {

TZeroXBufRd.ar(

b, z,

trig: Impulse.ar(MouseX.kr(30, ~maxTrigRate)),

zeroX: Dseq([2, 3], inf),

power: MouseY.kr(0.3, 2),

rate: 0.2,

dir: Dseq([1, 1, 1, -1], inf),

).tanh * 0.3

}.play

)


x.release



Ex.2) The 'xNum' and 'xRep' args


// needs Buffers from Ex.1


// With ZeroXBufRd repetitions of (groups of) half wavesets can easily be defined

// with passing appropriate demand rate ugens to the zeroX arg.

// With TZeroXBufRd the situation is different as the use of an external trigger

// opens the freedom to specify the sequences of such groups independently.


// The following plots show the options in the case of non-overlapping groups.



// Half waveset and full waveset at indices 2 and 3


(

{

TZeroXBufRd.ar(

b, z,

trig: Impulse.ar(100),

xNum: Dseq([1, 2], inf),

zeroX: Dseq([2, 3], inf),

rate: 0.2

);

}.plot(0.03)

)


// Repeated half wavesets


(

{

TZeroXBufRd.ar(

b, z,

trig: Impulse.ar(50),

xRep: 3,

zeroX: Dseq([2, 3], inf),

rate: 0.2

);

}.plot(0.05)

)



// Sequencing half waveset number and repetitions


(

{

TZeroXBufRd.ar(

b, z,

trig: Impulse.ar(50),

xNum: Dseq([1, 2, 3], inf),

xRep: Dseq([3, 2, 1], inf),

zeroX: Dseq([2, 3], inf),

rate: 0.2

);

}.plot(0.14)

)





Ex.3) The 'overlapSize' arg



// TZeroXBufRd enables overlappings of half wavesets, the maximum number of

// overlaps is given with the non-modulatable arg 'overlapSize' which defaults to 10.

// So if waveset groups are too long and/or the trigger rate is too high

// groups will be cut and the synthesis, according to the other passed args, fails.


// Supposed constant values, the minimum necessary overlapSize can be calculated

// by the formula:


// ceiling((wavesetGroupSampleNum * trigRate) / (sampleRate * rate))


// where wavesetGroupSampleNum means the number of samples of a group,

// which consists of xNum * xRep half wavesets.



// It's the user's responsibility to care for a sufficiently large overlapSize,

// resp. sufficiently low trigger rates, xNum and xRep args as well as not too low playback rates.

// With recorded sounds it might be necessary to estimate the largest half wavesets

// by analysis in the language.


// An easy way to reduce the maximum and average half waveset length is 

// repeating the zeroX analysis with a LeakDC or high pass filter applied.



// Buffers from Ex.1

// We suppose a sample rate of 44100


b.loadToFloatArray(action: { |b| v = b })


z.loadToFloatArray(action: { |b| w = b })


// zero crossing analysis data


w


// with zeroX = 1 and xNum = 7 we have a chunk of 194 samples


w[8] - w[1]



// according to the above formula, with a trigger rate of 500 and

// a playback rate of 0.5 the minimal necessary overlapSize is 5


194 * 500 / (44100 * 0.5)


-> 4.3990929705215


// check with overlapSizes of 5 (or higher),

// it's the same and sounding smooth,

// with overlapSize = 4 the result is erroneous, resp. distorted


(

x = {

TZeroXBufRd.ar(

b, z,

trig: Impulse.ar(500),

zeroX: 1,

xNum: 7,

rate: 0.5,

overlapSize: 5,

) * 0.1

}.play

)


s.scope


x.release




Ex.4) Multichannel usage and the 'bufMix' arg


// This is very simliar to the conventions of ZeroXBufRd,

// buffers can be switched per trigger.


// Without passing a bufMix arg the size of the returned signal is determined by the buffer input. 

// It may be a single channel buffer or an array of single channel buffers, 

// in correspondence with the analysis buffer(s) - multichannel buffers are not allowed. 

// If bufMix is passed, it determines the size of the returned signal, 

// its components can be demand rate or other ugens to control switching between buffers per half waveset groups.


// Note: buffer switching can become CPU-demanding with a lot of Buffers 

// as for fast switching it is necessary to play all in parallel


(

// boot with extended resources

s = Server.local;

Server.default = s;

s.options.numWireBufs = 256; 

s.options.memSize = 8192 * 32; 

s.reboot;

)



// prepare 3 buffers


(

b = { Buffer.alloc(s, 1000, 1) } ! 3;

z = { Buffer.alloc(s, 100, 1) } ! 3;

)


// fill with basic waveforms

(

{

var src = [

SinOsc.ar(400),

LFTri.ar(400),

SinOsc.ar(400) ** 10

];

ZeroXBufWr.ar(src, b, z, startWithZeroX: 1, doneAction: 2);

Silent.ar

}.play

)



s.scope


// play 2 channels with sine and triangle, where

// triangle is alternating between half and full waveset


(

x = {

TZeroXBufRd.ar(

b, z,

trig: Impulse.ar(100),

xNum: [2, Dseq([1, 2], inf)],

zeroX: 0,

bufMix: [0, 1]

) * 0.1

}.play

)


x.release



// sequencing repetitions

// if both channels should get the same sequence wrap demand rate ugen into a Function


(

x = {

TZeroXBufRd.ar(

b, z,

trig: Impulse.ar(100),

xNum: 1,

xRep: { Dseq((1..5), inf) },

zeroX: 0,

bufMix: [0, 1],

rate: 0.7

) * 0.1

}.play

)


x.release



// overlapping groups


(

x = {

TZeroXBufRd.ar(

b, z,

trig: Impulse.ar(100),

xNum: 1,

xRep: [Dseq((1..15), inf), Dseq((15..1), inf)],

zeroX: 0,

bufMix: [0, 1],

rate: MouseX.kr(0.2, 3),

overlapSize: 10

) * 0.1

}.play

)


x.release



// estimate the maximum xRep for the given overlapSize,

// in the above example (suppose samplerate 44100):

// as the buffers have been generated with freq = 400,

// we can roughly estimate the half wavesets with 55 samples.


// According to the formula of Ex. 2


55 * xRep * 100 / (44100 * 0.2) = 10


xRep = (44100 * 0.2) * 10 / (55 * 100)


-> 16.036363636364


// so with xRep values up to 17 we get a bit of distortion besides the aliasing (hardly audible),

// it gets stronger with lower overlapSize and disappears with higher values


(

x = {

TZeroXBufRd.ar(

b, z,

trig: Impulse.ar(100),

xNum: 1,

xRep: [Dseq((1..17), inf), Dseq((17..1), inf)],

zeroX: 1,

bufMix: [0, 1],

rate: 0.2,

overlapSize: 10   // check with higher and lower values

) * 0.1

}.play

)


x.release




// alternating buffers in one channel

(

x = {

TZeroXBufRd.ar(

b, z,

trig: Impulse.ar(100),

xNum: 2,

xRep: 1,

zeroX: 1,

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

rate: 0.6,

) * 0.1

}.play

)


x.release



// various sequences in both channels with switched waveforms


(

x = {

TZeroXBufRd.ar(

b, z,

trig: Impulse.ar(200),

xNum: [Dseq([1, 2], inf), Dseq([2, 1], inf)],

xRep: [Dseq([1, 2], inf), Dseq([2, 1], inf)],

zeroX: 0,

bufMix: [Dseq([0, 1, 2], inf), Dseq([1, 2, 0], inf)],

rate: MouseX.kr(0.2, 2),

) * 0.1

}.play

)


x.release



Ex.5) The overall envelope


// This works also like for ZeroXBufRd, 'length' refers to the maximum number of triggers for half waveset groups.

 

// The finishing of a TZeroXBufRd is not detemined by finite demand rate ugens but by an overall envelope, 

// its release section is triggered by a maximum number of half wavesets ('length') or a maximum time. 


// Buffers from Ex.4


{ TZeroXBufRd.ar(b[0], z[0], trig: Impulse.ar(500), rate: 1, length: 10, rel: 0.01) }.plot(0.05)


{ TZeroXBufRd.ar(b[0], z[0], trig: Impulse.ar(500),  rate: 1, maxTime: 0.02, rel: 0.01) }.plot(0.05)



// envelopes can be differentiated


{ TZeroXBufRd.ar(b[0..1], z[0..1], trig: Impulse.ar(500), rate: 1, maxTime: [0.01, 0.005], rel: [0.005, 0.02]) }.plot(0.03)


{ TZeroXBufRd.ar(b[0..1], z[0..1], trig: Impulse.ar(500), rate: 1, length: [7, 2], rel: [0.005, 0.02]) }.plot(0.03)



// there should be only one doneAction 2 in this case


{ TZeroXBufRd.ar(b[0..1], z[0..1], trig: Impulse.ar(500), rate: 1, maxTime: [0.01, 0.005], rel: [0.05, 0.5], doneAction: [0, 2]) }.play



(

b.do(_.free);

z.do(_.free);

)



Ex.6) Simultaneous writing and reading



// The reading of half wavesets can start before analysis is finished,

// if TZeroXBufRd is carefully used in with a bit of delay.

// A bit more delicate than with ZeroXBufRd because of the independent trigger.



// prepare buffers


(

p = Platform.resourceDir +/+ "sounds/a11wlk01.wav";

b = Buffer.read(s, p);

)


(

z = Buffer.alloc(s, b.duration * 44100 / 5, 1);

s.scope;

)


// Simultaneous writing and reading is easier with ZeroXBufRd 

// as the trigger deltas are given by the source then.


(

{

var src = PlayBuf.ar(1, b, BufRateScale.ir(b));

// write zero crossings, but no need to overwrite sound buffer

ZeroXBufWr.ar(src, b, z, startWithZeroX: 1, writeSndBuf: 0);

DelayL.ar(

TZeroXBufRd.ar(

b, z,

// indicating stereo

bufMix: [0, 0],

// one trigger for both channels

// sufficiently slow progress for the given source

// (can not be guaranteed for an arbitrary signal)

trig: TDuty.ar(Dstutter(5, Dseq([1, 5, 10], inf)) * ControlDur.ir),

xNum: 1,

xRep: 2,

// stereo, drate ugens must be wrapped

zeroX: { Dseries() },

rate: { Dwhite(0.5, 0.7) }, 

maxTime: 7,

rel: 1,

doneAction: 2

),

0.1,

0.1

);

}.play

)


(

b.free;

z.free;

)


// It's of course unproblematic – and still quasi realtime – to fully a analyse 

// a snippet of sound with ZeroXBufWr before freely using TZeroXBufRd in the same synth



Ex.7) Granulation with movement through a buffer


// See Buffer Granulation tutorial, Ex. 1h