Event patterns and array args setting and passing arrays and envelopes via patterns
Part of: miSCellaneous
This tutorial covers some use cases of array args, especially passing arrays to synths with patterns.
Examples
1.) Alternative writing of arrayed args in SynthDefs
(
s = Server.local;
Server.default = s;
s.boot;
)
// SynthDef with array of overtone weights,
// weights can additionally be influenced with a dampExp arg unequal to 0,
// see examples below.
(
// The standard way to define array args:
// when appearing within the list of args, a literal Array must be used,
// shortcut (1..8) generates an Array with Integers from 1 to 8
SynthDef(\array_1a, { |out = 0, freq = 440, otAmps = #[1,1,1,1,1,1,1,1], dampExp = 0,
att = 0.01, rel = 0.6, amp = 0.1, gate = 1, freqLag = 0.02, otLag = 0.02|
var sig, env, freqs, amps;
freqs = (freq * (1..8)).clip(20, 20000).lag(freqLag);
amps = ((otAmps / ((1..8) ** dampExp)).normalizeSum * amp).lag(otLag);
sig = SinOsc.ar(freqs, 0, amps);
env = EnvGen.ar(Env.asr(att, 1, rel), gate, doneAction: 2);
Out.ar(out, Splay.ar(sig) * env)
}).add
)
// Note that SynthDef \array_1a uses the array arg's size (8) at two further places (arrays (1..8)).
// As a literal Array, by its nature, requires the explicit writing of its items,
// this becomes arkward with larger arrays and/or rewriting the SynthDef with other sizes.
// For those reasons the use of NamedControl turns out to be very convenient with array args,
// it also gives an easy way of lagging.
(
// Alternative to SynthDef \array_1a using NamedControl:
// Although a NamedControl can be written at any position within the SynthDef,
// it's a useful convention to invent it directly after the args list
// by assigning it to a variable of the same name (so code with literal Array args can be reused easily).
// With NamedControl the array size can now also be written as a variable,
// which allows to define SynthDefs with different array sizes on the fly.
// Also note NamedControl's shortcuts aSymbol.kr / aSymbol.kr
// which make its use even more comfortable.
// define size for SynthDef
n = 8;
// define SynthDef
// shortcut 1!n (short for 1.dup(n)) generates an Array of size n filled with 1.
SynthDef(\array_1b, { |out = 0, freq = 440, dampExp = 0,
att = 0.01, rel = 0.6, amp = 0.1, gate = 1, freqLag = 0.02, otLag = 0.02|
var otAmps = NamedControl.kr(\otAmps, 1!n); // shortcut: otAmps = \otAmps.kr(1!n);
var sig, env, freqs, amps;
freqs = (freq * (1..n)).clip(20, 20000).lag(freqLag);
amps = ((otAmps / ((1..n) ** dampExp)).normalizeSum * amp).lag(otLag);
sig = SinOsc.ar(freqs, 0, amps);
env = EnvGen.ar(Env.asr(att, 1, rel), gate, doneAction: 2);
Out.ar(out, Splay.ar(sig) * env)
}).add
)
// As array sizes of SynthDefs are fixed you might want to define for a number
// of Integers and name the SynthDefs appropriately.
// Here's such a "SynthDef factory":
// we get SynthDefs of names \array_1b_1, ..., \array_1b_16 with corresponding array sizes,
// see Ex. 3b for a use case.
(
(1..16).do { |n|
var name = \array_1b ++ \_ ++ n;
SynthDef(name, { |out = 0, freq = 440, dampExp = 0,
att = 0.01, rel = 0.6, amp = 0.1, gate = 1, freqLag = 0.02, otLag = 0.02|
var otAmps = NamedControl.kr(\otAmps, 1!n); // shortcut: otAmps = \otAmps.kr(1!n);
var sig, env, freqs, amps;
freqs = (freq * (1..n)).clip(20, 20000).lag(freqLag);
amps = ((otAmps / ((1..n) ** dampExp)).normalizeSum * amp).lag(otLag);
sig = SinOsc.ar(freqs, 0, amps);
env = EnvGen.ar(Env.asr(att, 1, rel), gate, doneAction: 2);
Out.ar(out, Splay.ar(sig) * env)
}).add
}
)
2.) Setting array args and array fields of a running synth
Ex. 2a: Setting array args of a running synth
// start Synth, SynthDefs \array_1a, \array_1b and \array_1b_8 are equivalent
x = Synth(\array_1a, [freq: 300, amp: 0.2])
// only odd partials (clarinet-like)
x.set(\otAmps, [1, 0, 1, 0, 1, 0, 1, 0])
// emphasize one partial
x.set(\otAmps, [1, 0, 1, 0, 1, 5, 1, 0])
// only lower ones
x.set(\otAmps, [1, 1, 1, 1, 0, 0, 0, 0])
// default again
x.set(\otAmps, [1, 1, 1, 1, 1, 1, 1, 1])
// force lower partials with dampExp > 0
x.set(\dampExp, 1.5)
// force higher partials with dampExp < 0
x.set(\dampExp, -1.5)
// default again
x.set(\dampExp, 0)
// release
x.release
Ex. 2b: Setting array args of a running synth with Pbind of event type \set
// For many use cases Pmono (see 2c) is the most practical solution
// as it doesn't require explicit starting of a synth.
// However sometimes it is necessary to access the running synth itself,
// then a Pbind with event type \set is a good choice.
// start Synth silently
x = Synth(\array_1a, [freq: 200, amp: 0])
// Pbind of event type set:
// args to be set must be listed.
// Note that the array arg requires double brackets (syntactic distinction from setting multiple nodes)
(
p = Pbind(
\dur, 0.2,
\type, \set,
\id, x,
\args, #[freq, amp, dampExp, otAmps], // must be given explicitely
\midinote, Pwhite(45.0, 70), // for midinote freq must be set
\amp, 0.3,
\dampExp, Pwhite(0, 3),
\otAmps, [[2, 1, 3, 2, 1, 2, 2, 1]] // double brackets for array arg !
).play
)
(
// stop EventStreamPlayer and release Synth
p.stop;
x.release;
)
// start Synth silently
x = Synth(\array_1a, [freq: 200, amp: 0]);
// Sequencing the array arg:
// rising spectral shape, falling fundamental
(
// Array for Pseq, we need an array of doubly bracketed arrays or
// ref'd arrays as in Ex. 2c.
o = [
[[1, 1, 0, 0, 0, 0, 0, 0]],
[[1, 0, 1, 0, 0, 0, 0, 0]],
[[1, 1, 0, 1, 0, 0, 0, 0]],
[[1, 0, 1, 0, 1, 0, 0, 0]],
[[1, 1, 0, 1, 0, 1, 0, 0]],
[[1, 0, 1, 0, 1, 0, 1, 0]],
[[1, 0, 0, 1, 0, 1, 0, 1]]
];
p = Pbind(
\dur, 0.2,
\type, \set,
\id, x,
\args, #[freq, amp, otAmps], // must be given explicitely
\midinote, Pn(Plazy { Pseq((70..50)) / rrand(1, 1.2) }),
\amp, 0.3,
\otAmps, Pseq(o, 30)
).play
)
(
// stop EventStreamPlayer and release Synth
p.stop;
x.release;
)
Ex. 2c: Setting array args of a running synth with Pmono
// Rewriting second example of 2b, shorter with Pmono.
// Instead of doubly bracketed arrays you can choose Refs of Arrays also.
// Note that this alternative is not valid for a single nested Array as
// in the first example of 2b.
(
o = [
`[1, 1, 0, 0, 0, 0, 0, 0],
`[1, 0, 1, 0, 0, 0, 0, 0],
`[1, 1, 0, 1, 0, 0, 0, 0],
`[1, 0, 1, 0, 1, 0, 0, 0],
`[1, 1, 0, 1, 0, 1, 0, 0],
`[1, 0, 1, 0, 1, 0, 1, 0],
`[1, 0, 0, 1, 0, 1, 0, 1]
];
p = Pmono(\array_1a,
\dur, 0.2,
\midinote, Pn(Plazy { Pseq((70..50)) / rrand(1, 1.2) }),
\amp, 0.3,
\otAmps, Pseq(o, 30)
).play
)
// stop EventStreamPlayer from Pmono (synth is released also)
p.stop;
Ex. 2d: Setting i-th fields of a running synth
// start Synth
x = Synth(\array_1a, [freq: 200, amp: 0.3])
// by default otAmps equals [1, 1, 1, 1, 1, 1, 1, 1]
// Since SC 3.6.x there exists method seti for setting single elements of arrays:
// turn off high overtones
x.seti(\otAmps, 7, 0)
x.seti(\otAmps, 6, 0)
x.seti(\otAmps, 5, 0)
x.release;
// With older SC versions you can use the following helper functions to achieve the same.
// This requires that the SynthDef has been added in order to have control arg info in SynthDescLib.
(
~getCtlIndex = { |defName, argName, index|
var x = SynthDescLib.global.at(defName.asSymbol).controls
.collect({|x| x.name.asSymbol }).indexOf(argName.asSymbol);
x !? { x + index };
};
~setiID = { |server, defname, nodeID, key, index, value|
server.sendMsg(15, nodeID, ~getCtlIndex.(defname, key, index), value);
};
~seti = { |node, key, index, value|
~setiID.(node.server, node.defName, node.nodeID, key, index, value);
};
)
// start Synth
x = Synth(\array_1a, [freq: 200, amp: 0.5])
// turn off high overtones
~seti.(x, \otAmps, 7, 0)
~seti.(x, \otAmps, 6, 0)
~seti.(x, \otAmps, 5, 0)
x.release;
Ex. 2e: Setting i-th fields of a running synth with patterns
// For SC versions before invention of method seti use helper functions from Ex. 2d in Pbind.
// start Synth
x = Synth(\array_1a, [freq: 200, amp: 0.3])
// sequence setting of single fields
// As this is currently not integrated with Pmono or event type \set
// it must be done explicitely. Method 'makeBundle' with server latency arg
// ensures that array field setting is done in parallel to
// other settings triggered by the event.
(
// continously subtract and add 7 overtones
p = Pbind(
\type, \rest,
\dur, 0.2,
\otAmp, Pstutter(7, Pseq([0,1], inf)),
\amp, 0.3,
\i, Pn(Pshuf((1..7))),
// use Function ~seti (2d) for SC versions without method seti:
\do, Pfunc { |e| s.makeBundle(s.latency, { ~seti.(x, \otAmps, e.i, e.otAmp) }) }
// for newer SC versions with method seti you can use this line instead:
// \do, Pfunc { |e| s.makeBundle(s.latency, { x.seti(\otAmps, e.i, e.otAmp) }) }
).trace.play
)
// stop player and synth
(
p.stop;
x.release;
)
// This approach can be extended by setting more than one field per event.
// In some cases this might be a reasonable alternative to setting whole arrays
// of running synths (2b, 2c), which requires more OSC traffic.
// start Synth from Ex. 1 with 16 overtones
x = Synth(\array_1b_16, [freq: 100, amp: 0.3])
(
// lists for bookkeeping of substracted and added overtones
a = (1..16).asList;
b = List[];
// Function to shovel indices from list to list
f = { |list1, list2|
var x = list1.choose;
list1.remove(x);
list2.add(x);
x
};
// continously subtract and add 5 x 3 overtones
p = Pbind(
\type, \rest,
\dur, 0.2,
\otAmp, Pstutter(5, Pseq([0,1], inf)),
\amp, 0.3,
// shoveling indices from a to b and back, outputting sorted index tripels
\i, Pseq([
Pfinval(5, Pclump(3, Pfunc { f.(a, b) } )),
Pfinval(5, Pclump(3, Pfunc { f.(b, a) } ))
], inf).collect(_.sort),
// use Function ~seti (2d) for SC versions without method seti:
\do, Pfunc { |e| s.makeBundle(s.latency, { e.i.do { |j| ~seti.(x, \otAmps, j, e.otAmp) } }) }
// for newer SC versions with method seti you can use this line instead:
// \do, Pfunc { |e| s.makeBundle(s.latency, { e.i.do { |j| x.seti(\otAmps, j, e.otAmp) } }) }
).trace.play
)
// stop player and synth
(
p.stop;
x.release;
)
Ex. 2f: Alternatives with demand ugens
// Besides from passing arrays, array sequencing can be
// done within synths by demand ugens.
// This is saving OSC bandwidth, especially with large arrays and short durations,
// for more complicated sequencing tasks coding might be harder than with patterns.
(
// array sequencing within SynthDef
q = [
[1, 1, 0, 0, 0, 0, 0, 0],
[1, 0, 1, 0, 0, 0, 0, 0],
[1, 1, 0, 1, 0, 0, 0, 0],
[1, 0, 1, 0, 1, 0, 0, 0],
[1, 1, 0, 1, 0, 1, 0, 0],
[1, 0, 1, 0, 1, 0, 1, 0],
[1, 0, 0, 1, 0, 1, 0, 1]
];
// duration will have to be passed with a key unequal to reserved keyword \dur
SynthDef(\array_1c, { |out = 0, freq = 440, dampExp = 0, duration = 0.2,
att = 0.01, rel = 0.6, amp = 0.1, gate = 1, freqLag = 0.02, otLag = 0.02|
var otAmps, sig, env, freqs, amps, arr;
otAmps = Demand.kr(Impulse.kr(1 / duration), 0, Dseq(q, inf));
freqs = (freq * (1..8)).clip(20, 20000).lag(freqLag);
amps = ((otAmps / ((1..8) ** dampExp)).normalizeSum * amp).lag(otLag);
sig = SinOsc.ar(freqs, 0, amps);
env = EnvGen.ar(Env.asr(att, 1, rel), gate, doneAction: 2);
Out.ar(out, Splay.ar(sig) * env)
}).add;
// SynthDef for complete server-side sequencing
// takes midiseq as array arg
SynthDef(\array_1d, { |out = 0, dampExp = 0, duration = 0.2, divLo = 1, divHi = 1.2,
att = 0.01, rel = 0.6, amp = 0.3, gate = 1, freqLag = 0.02, otLag = 0.02|
var midiseq = \midiseq.kr((70..50));
var trig, otAmps, sig, env, freq, freqs, amps, arr;
trig = Impulse.kr(1 / duration);
otAmps = Demand.kr(trig, 0, Dseq(q, inf));
freq = Demand.kr(trig, 0, Dseq(midiseq, inf) /
Dstutter(midiseq.size, Dwhite(divLo, divHi))).midicps;
freqs = (freq * (1..8)).clip(20, 20000).lag(freqLag);
amps = ((otAmps / ((1..8) ** dampExp)).normalizeSum * amp).lag(otLag);
sig = SinOsc.ar(freqs, 0, amps);
env = EnvGen.ar(Env.asr(att, 1, rel), gate, doneAction: 2);
Out.ar(out, Splay.ar(sig) * env)
}).add
)
(
// equivalent to Ex 2c
// still using a pattern for non-array sequencing
p = Pmono(\array_1c,
\dur, 0.2,
\duration, Pkey(\dur),
\midinote, Pn(Plazy { Pseq((70..50)) / rrand(1, 1.2) }),
\amp, 0.3
).play
)
p.stop;
// equivalent, now all done by synth
x = Synth(\array_1d)
// set midi sequence
x.set(\midiseq, (50..70))
x.set(\midiseq, (70..50))
x.set(\midiseq, (70..50).scramble)
x.release
3.) Sequencing synths with array args by Pbind
Ex. 3a: Sequencing synths of same definition with array args by Pbind
// Using a Pbind with array args is straightforward,
// it's just important to remember that arrays must be in
// double brackets (or wrapped into Refs).
// Written explicitely it looks a bit odd as you end up with
// three brackets at begin and end:
// Pseq([[[1, 1, 0, 0, 0, 0, 0, 0]], ... , [[1, 0, 0, 1, 0, 1, 0, 1]]], 100)
(
// Array o from definitions in Ex. 2b / 2c.
o = [
[[1, 1, 0, 0, 0, 0, 0, 0]],
[[1, 0, 1, 0, 0, 0, 0, 0]],
[[1, 1, 0, 1, 0, 0, 0, 0]],
[[1, 0, 1, 0, 1, 0, 0, 0]],
[[1, 1, 0, 1, 0, 1, 0, 0]],
[[1, 0, 1, 0, 1, 0, 1, 0]],
[[1, 0, 0, 1, 0, 1, 0, 1]]
];
// generating multiple nodes per event is also no problem,
// arrays from o are sent to both nodes here:
p = Pbind(
\instrument, \array_1a,
\dur, 0.2,
\amp, 0.3,
\stepsPerOctave, 5,
\octave, 4,
\note, Pstutter(7, Pn(Pshuf((0..4)))) + [0, -4],
\otAmps, Pseq(o, 100)
).trace.play
)
p.stop;
Ex. 3b: Sequencing synths with different array arg sizes by Pbind
// By using the Array o in the last examples we did a kind of zeropadding:
// setting unused elements to zero.
// When altering one running synth (as with event type \set or Pmono)
// one can only save OSC messages if less than all fields are changed, see Ex. 2e.
// When continuously generating new synths – as with 'normal' Pbind of type \note –
// there is another alternative: using SynthDefs of dedicated array arg sizes per event.
// This is the use case of a "SynthDef factory" as described in Ex. 1
(
// needs SynthDefs from "factory" in Ex. 1
q = [
[[1, 1, 1]],
[[1, 1, 1, 1, 1]],
[[1, 0, 1, 0, 1, 0, 1, 0, 1]],
[[1, 1, 0, 1, 0, 1, 0, 1, 0, 1]]
];
p = Pbind(
\otAmps, Pn(Pshuf(q), 100),
\size, Pkey(\otAmps).collect { |x| x[0].size.asSymbol },
\instrument, Pkey(\size).collect { |x| \array_1b_ ++ x }, // SynthDef depends on chosen otAmp array
\dur, 0.2,
\amp, 0.3,
\stepsPerOctave, 7,
\octave, 4,
\note, Pstutter(3, Pn(Pshuf((0..4)))) + Pn(Pshuf([[0, 2], [0, 4, 8], [0, -3, -5, -8]]))
).trace.play
)
p.stop;
Ex. 4: Sequencing synths with envelope array args by Pbind
// Env objects have a representation in a special Array format –
// which you can get with anEnv.asArray – the task of passing Env data to synths can thus be
// reduced to the task of passing Arrays in this special format.
// Nevertheless direct passing of Envs is possible also.
(
// NamedControl is recommended in that case as a literal Array of special format would be impractical.
// Define a SynthDef with maximum envelope size you expect to pass
SynthDef(\envArray_1, { |out = 0, freq = 440, amp = 0.1, timeScale = 1, gate = 1|
var sig, env, envArray, envSig;
envArray = Env([0, 1, 0, 0, 0, 0, 0], [1, 1, 0, 0, 0, 0]).asArray; // works also without '.asArray'
env = \env.kr(envArray); // shortcut for NamedControl.kr(\env, envArray)
envSig = EnvGen.kr(env, gate, timeScale: timeScale, doneAction: 2);
sig = Saw.ar([freq, freq * 2.01], amp);
Out.ar(out, sig * envSig)
}).add
)
// Pbind uses event type \on to avoid setting gate to 0 and receiving messages "node not found",
// synths are ended by envelopes anyway.
(
p = Pbind(
\type, Pshuf([\on, \on, \rest], inf),
\instrument, \envArray_1,
\dur, Pn(Pshuf([1, 1/2, 1/2]), 30),
\midinote, Pn(Pshuf((40..80))) + Pn(Pshuf([[0.5, 11.5], [0, 4], [0, -7], [-0.5, -9.5]])),
// envData contains env types, determined by levels and times.
// Times are only relations, within the synthdef they are scaled by the timeScale arg.
\envData, Pn(Pshuf([
[[0, 1, 0], [1, 1]],
[[0, 1, 1/4, 1, 0], [1, 1, 1, 1]],
[[0, 1, 1/4, 1, 1/4, 1, 0], [1, 1, 1, 1, 1, 1]]
])),
// *x splits the envData array into levels and times, Env is converted in to an Array automatically
\env, Pkey(\envData).collect { |x| Env(*x) },
// when wanting to pass the Array explicitely it would require
// wrapping into an additional Array which is necessary for passing:
// \env, Pkey(\envData).collect { |x| [Env(*x).asArray] },
\timeScale, Pfunc { |e| e.dur / e.envData[1].sum }
).trace.play;
)
p.stop;