Skip to content

Instantly share code, notes, and snippets.

@scztt
Last active September 17, 2021 14:03
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save scztt/51e19767ae954adfa8a5783dce183209 to your computer and use it in GitHub Desktop.
Save scztt/51e19767ae954adfa8a5783dce183209 to your computer and use it in GitHub Desktop.
Pmod.sc
(
SynthDef(\saw, {
var env, sig;
sig = Saw.ar(
\freq.kr(440) * (\fmod.ar(0).midiratio * [-1, 1]),
\amp.kr(1)
);
env = Env.adsr(releaseTime: \release.kr(1));
env = env.kr(
gate: \gate.kr(1),
timeScale: \sustain.kr,
doneAction: 2
);
sig = \lpf.kr([100, 3000]).collect {
|lpf, i|
RLPF.ar(
sig[i],
lpf,
0.6
)
};
sig = LeakDC.ar(sig);
sig = Rotate2.ar(sig[0], sig[1], pi + (SinOsc.kr(1/2) * 0.2));
sig = env * sig;
sig = Balance2.ar(
sig[0], sig[1],
\pan.kr(0)
);
Out.ar(\out.ir, sig);
}).add;
Pdef(\base, Pbind(
\instrument, \saw,
\octave, [4, 4],
\dur, 1/6,
\release, Pwhite(1, 2.9),
\legato, Pwhite(0.7, 1.9),
\scale, Scale.harmonicMinor,
\degree, Ptuple([
0,
Pseq([
Pseq([-2, 3, 7, 3, -4], 8),
Pseq([-2, 3, 7, 3, -4] + [-3, 0], 8),
], inf)
], inf),
));
)
// Basic kr
(
Pdef(\basicMod, Pbind(
\lpf, Pmod({
SinOsc.kr(1/8, [0, 0.3]).exprange(420, 5000)
}),
) <> Pdef(\base)).play;
)
// Explicit rate
(
Pdef(\basicMod, Pbind(
\lpf, Pmod.kr({
SinOsc.kr(1/8, [0, 0.3]).exprange(420, 5000)
}),
) <> Pdef(\base)).play;
)
// Explicit rate and channels, with coercion
(
Pdef(\basicMod, Pbind(
\lpf, Pmod.kr2({
SinOsc.ar(1/8, 0).exprange(420, 5000)
}),
) <> Pdef(\base)).play;
)
// Single values with lag
(
Pdef(\basicMod, Pbind(
\lpf, Pmod(
Pseq([300, 800, 4000], inf).stutter(3),
\paramLag, 0.1
),
) <> Pdef(\base)).play;
)
// Resend
(
Pdef(\basicMod, Pbind(
\lpf, Pmod(
{ SinOsc.kr(Rand(4, 12)).exprange(100, 1000) },
\resend, Pseq([false, false, false, true], inf)
),
) <> Pdef(\base)).play;
)
// Resend with fadeTime
(
Pdef(\basicMod, Pbind(
\lpf, Pmod(
{ SinOsc.kr(Rand(1, 8)).exprange(100, 5000) },
\resend, Pseq([true] ++ (false ! 10), inf),
\fadeTime, 4
),
) <> Pdef(\base)).play;
)
// Basic ar
(
Pdef(\basicMod, Pbind(
\fmod, Pmod({
SinOsc.ar(1/8) * SinOsc.ar(200).range(-0.5, 0.5)
}),
) <> Pdef(\base)).play;
)
// Pattern kr
(
Pdef(\basicMod, Pbind(
\lpf, Pmod(
{ \f.kr(100).lag3([0.01, 1]) },
\f, Pexprand(120, 8000).stutter(3)
)
) <> Pdef(\base)).play;
)
// Pattern kr with two filters
(
Pdef(\basicMod, Pbind(
\lpf, Pmod(
{ \f.kr(100).lag3([0.01, 1]) },
\f, Ptuple([
Pexprand(120, 8000).stutter(4),
Pexprand(120, 8000).stutter(6)
], inf)
)
) <> Pdef(\base)).play;
)
// Pattern kr as fixed values
(
Pdef(\basicMod, Pbind(
\lpf, Pmod(
{ \f.kr(100).lag3([0.01, 1]) },
\f, Ptuple([
Pexprand(120, 8000).stutter(4),
Pexprand(120, 8000).stutter(6)
], inf)
).asValues
) <> Pdef(\base)).play;
)
// Pmod modulating Pmod
(
Pdef(\basicMod, Pbind(
\lpf, Pmod(
{ \f.kr(100).lag3([0.01, 1]) },
\f, Pmod.kr({
LFDNoise3.kr(1).exprange(80, 12000)
})
)
) <> Pdef(\base)).play;
)
// 2 Pmods modulating Pmod
// Note: expand converts a 2-channel modulator into an arrayed value.
// This causes channel expansion when event processing - it is
// equivalent to eg. \freq, [100, 200] spawning two events,
// rather than \freq, [[100, 200]] passing an array value to a
// single synth arg.
(
Pdef(\basicMod, Pbind(
\lpf, Pmod(
{ \f.kr(100).lag3([0.01, 1]) },
\f, Pmod.kr({
LFDNoise3.kr([1, 6]).exprange(80, 12000)
}).expand
)
) <> Pdef(\base)).play;
)
// Pmono
(
Pdef(\basicMod, Pmono(
\saw,
\degree, Pfunc({ |e| e.degree +.x [0, 0.02] }),
\fmod, Pmod({
Decay.kr(\trig.tr, 0.2) * SinOsc.ar(200).range(-15, 15)
}, \trig, Pseq([1, 0, 0, 1, 0, 0, 0], inf)),
\lpf, Pmod(
{ \f.kr(100).lag3([0.01, 1]) },
\f, Pmod.kr({
LFDNoise3.kr([1, 6]).exprange(80, 12000)
}).expand
)
) <> Pdef(\base)).play;
)
// \type, \set
// requires a previous Pmod to be defined
(
Ndef(\basicMod, \saw).play;
Pdef(\set, Pbind(
\type, \set,
\args, [\pan, \freq],
\id, Ndef(\basicMod).group,
\group, Ndef(\basicMod).group,
\pan, Pmod({
LFPulse.kr(1/3).range(-0.5, 0.5).lag(0.1)
})
) <> Pdef(\base)).play
)
Pmod : Pattern {
classvar defHashLRU, <defCache, <defNames, <defNamesFree, defCount=0, maxDefNames=100;
var <>synthName, <>patternPairs, <rate, <>channels, asValues=false;
*new {
|synthName ... pairs|
^super.newCopyArgs(synthName, pairs)
}
*kr {
|synthName ... pairs|
^this.new(synthName, *pairs).rate_(\control)
}
*kr1 {
|synthName ... pairs|
^this.new(synthName, *pairs).rate_(\control).channels_(1)
}
*kr2 {
|synthName ... pairs|
^this.new(synthName, *pairs).rate_(\control).channels_(2)
}
*kr3 {
|synthName ... pairs|
^this.new(synthName, *pairs).rate_(\control).channels_(3)
}
*kr4 {
|synthName ... pairs|
^this.new(synthName, *pairs).rate_(\control).channels_(4)
}
*ar {
|synthName ... pairs|
^this.new(synthName, *pairs).rate_(\audio)
}
*ar1 {
|synthName ... pairs|
^this.new(synthName, *pairs).rate_(\audio).channels_(1)
}
*ar2 {
|synthName ... pairs|
^this.new(synthName, *pairs).rate_(\audio).channels_(2)
}
*ar3 {
|synthName ... pairs|
^this.new(synthName, *pairs).rate_(\audio).channels_(3)
}
*ar4 {
|synthName ... pairs|
^this.new(synthName, *pairs).rate_(\audio).channels_(4)
}
*initClass {
defCache = ();
defNames = ();
defHashLRU = LinkedList();
defNamesFree = IdentitySet();
(1..16).do {
|n|
[\kr, \ar].do {
|rate|
this.wrapSynth(
rate: rate,
func: { \value.perform(rate, (0 ! n)) },
channels: n,
defName: "Pmod_constant_%_%".format(n, rate).asSymbol,
);
}
}
}
// Wrap a func in fade envelope / provide XOut
*wrapSynth {
|rate, func, channels, defName|
var hash, def, args;
defName = defName ?? {
hash = [func, rate].hash;
defHashLRU.remove(hash);
defHashLRU.addFirst(hash);
defNames[hash] ?? {
defNames[hash] = this.getDefName();
defNames[hash]
};
};
if (defCache[defName].isNil) {
def = SynthDef(defName, {
var fadeTime, paramLag, fade, sig;
fadeTime = \fadeTime.kr(0);
paramLag = \paramLag.ir(0);
fade = Env([1, 1, 0], [0, fadeTime], releaseNode:1).kr(gate:\gate.kr(1), doneAction:2);
sig = SynthDef.wrap(func, paramLag ! func.def.argNames.size);
sig = sig.asArray.flatten;
if (channels.isNil) {
channels = sig.size;
};
if (rate.isNil) {
rate = sig.rate.switch(\audio, \ar, \control, \kr);
};
\channels.ir(channels); // Unused, but helpful to see channelization for debugging
sig = sig.collect {
|channel|
if ((channel.rate == \scalar) && (rate == \ar)) {
channel = DC.ar(channel);
};
if ((channel.rate == \audio) && (rate == \kr)) {
channel = A2K.kr(channel);
"Pmod output is \audio, \control rate expected".warn;
} {
if ((channel.rate == \control) && (rate == \ar)) {
channel = K2A.ar(channel);
"Pmod output is \control, \audio rate expected".warn;
}
};
channel;
};
if (sig.shape != [channels]) {
sig.reshape(channels);
};
XOut.perform(rate, \out.kr(0), fade, sig);
});
args = def.asSynthDesc.controlNames.flatten.asArray;
defCache[defName] = [rate, channels, def, args];
} {
#rate, channels, def, args = defCache[defName];
};
def.add;
^(
instrument: defName,
args: [\value, \fadeTime, \paramLag, \out] ++ args,
pr_rate: rate,
pr_channels: channels,
pr_instrumentHash: hash ?? { [func, rate].hash },
hasGate: true
)
}
rate_{
|r|
rate = (
control: \kr,
audio: \ar,
kr: \kr,
ar: \kr
)[r]
}
embedInStream {
|inEvent|
var server, synthStream, streamPairs, endVal, cleanup,
synthGroup, newSynthGroup, modGroup, newModGroup,
buses, currentArgs, currentBuses, currentSize, currentEvent, fadeTime,
nextEvent, nextSynth, streamAsValues, currentChannels, currentRate, cleanupFunc;
// CAVEAT: Server comes from initial inEvent and cannot be changed later on.
server = inEvent[\server] ?? { Server.default };
server = server.value;
streamAsValues = asValues;
// Setup pattern pairs
streamPairs = patternPairs.copy;
endVal = streamPairs.size - 1;
forBy (1, endVal, 2) { |i| streamPairs[i] = streamPairs[i].asStream };
synthStream = synthName.asStream;
// Prepare busses
buses = List();
// Cleanup
cleanupFunc = Thunk({
currentEvent !? {
if (currentEvent[\isPlaying].asBoolean) {
currentEvent.release(currentEvent[\fadeTime])
};
this.recycleDefName(currentEvent);
{
newModGroup !? _.free;
buses.do(_.free);
}.defer(currentEvent[\fadeTime] ? 10)
{
newSynthGroup !? _.free;
}.defer(5);
}
});
cleanup = EventStreamCleanup();
cleanup.addFunction(inEvent, cleanupFunc);
loop {
// Prepare groups, reusing input group if possible.
// This is the group that the outer event - the one whose parameters
// we're modulating - is playing to.
//
// If newSynthGroup.notNil, then we allocated and we must clean up.
if (inEvent.keys.includes(\group)) {
synthGroup = inEvent.use({ ~group.value });
} {
inEvent[\group] = synthGroup = newSynthGroup ?? {
newSynthGroup = Group(server.asTarget);
};
};
// Prepare modGroup, which is our modulation group and lives before
// synthGroup.
// If newModGroup.notNil, then we allocated and we must clean up
if (inEvent.keys.includes(\modGroup)) {
modGroup = inEvent[\modGroup];
} {
inEvent[\modGroup] = modGroup = newModGroup ?? {
newModGroup = Group(synthGroup.asTarget, \addBefore);
};
};
// We must set group/addAction early, so they are passed to the .next()
// of child streams.
nextEvent = ();
nextEvent[\synthDesc] = nil;
nextEvent[\msgFunc] = nil;
nextEvent[\group] = modGroup;
nextEvent[\addAction] = \addToHead;
nextEvent[\resend] = false;
// Get nexts
nextSynth = synthStream.next(nextEvent.copy);
nextSynth = this.prepareSynth(nextSynth);
nextEvent = this.prNext(streamPairs, nextEvent);
if (inEvent.isNil || nextEvent.isNil || nextSynth.isNil) {
^cleanup.exit(inEvent);
} {
cleanup.update(inEvent);
nextEvent.putAll(nextSynth);
// 1. We need argument names in order to use (\type, \set).
// 2. We need size to determine if we need to allocate more busses for e.g.
// an event like (freq: [100, 200]).
currentArgs = nextEvent[\instrument].asArray.collect(_.asSynthDesc).collect(_.controlNames).flatten.asSet.asArray;
currentSize = nextEvent.atAll(currentArgs).maxValue({ |v| v.isArray.if(v.size, 1) }).max(1);
currentChannels = nextSynth[\pr_channels];
currentRate = nextSynth[\pr_rate];
buses.first !? {
|bus|
var busRate = switch(bus.rate, \audio, \ar, \control, \kr, bus.rate);
if (busRate != currentRate) {
Error("Cannot use Synths of different rates in a single Pmod (% vs %)".format(
bus.rate, currentRate
)).throw;
}
};
(currentSize - buses.size).do {
if (currentRate == \ar) {
buses = buses.add(Bus.audio(server, currentChannels))
} {
buses = buses.add(Bus.control(server, currentChannels))
};
};
currentBuses = buses.collect(_.index).extend(currentSize);
if (currentBuses.size == 1) { currentBuses = currentBuses[0] };
// If we've got a different instrument than last time, send a new one,
// else just set the parameters of the existing.
if (nextEvent[\resend]
or: {nextEvent[\pr_instrumentHash] != currentEvent.tryPerform(\at, \pr_instrumentHash)})
{
nextEvent[\parentType] = \note;
nextEvent[\type] = \note;
nextEvent[\sustain] = nil;
nextEvent[\sendGate] = false;
nextEvent[\fadeTime] = fadeTime = nextEvent[\fadeTime] ?? 0;
nextEvent[\out] = currentBuses;
nextEvent[\group] = modGroup;
nextEvent[\addAction] = \addToHead; // SUBTLE: new synths before old, so OLD synth is responsible for fade-out
// Free existing synth
currentEvent !? {
|e|
// Assumption: If \hasGate -> false, then synth will free itself.
if (e[\isPlaying].asBoolean && e[\hasGate]) {
e[\sendGate] = true;
e.release(nextEvent[\fadeTime]);
e[\isPlaying] = false;
}
};
} {
nextEvent[\parentType] = \set;
nextEvent[\type] = \set;
nextEvent[\id] = currentEvent[\id];
nextEvent[\args] = currentEvent[\args];
nextEvent[\out] = currentEvent[\out];
};
nextEvent.parent ?? { nextEvent.parent = Event.parentEvents.default };
// SUBTLE: If our inEvent didn't have a group, we set its group here.
// We do this late so previous uses of inEvent aren't disrupted.
if (newSynthGroup.notNil) {
inEvent[\group] = newSynthGroup;
};
// Yield our buses via .asMap
inEvent = currentSize.collect({
|i|
var group;
{
if (i == 0) {
cleanup.addFunction(currentEnvironment, cleanupFunc)
};
// In this context, ~group refers to the event being modulated,
// not the Pmod event.
~group = ~group.value;
if (~group.notNil and: { ~group != synthGroup }) {
modGroup.moveBefore(~group.asGroup)
};
if (nextEvent[\isPlaying].asBoolean.not) {
currentEvent = nextEvent;
nextEvent[\isPlaying] = true;
nextEvent.playAndDelta(cleanup, false);
};
if (streamAsValues) {
buses[i].getSynchronous;
} {
buses[i].asMap;
}
}
});
if (currentSize == 1) {
inEvent = inEvent[0].yield;
} {
inEvent = inEvent.yield;
}
};
}
^cleanup.exit(inEvent);
}
// This roughly follows the logic of Pbind
prNext {
|streamPairs, inEvent|
var event, endVal;
event = this.prScrubEvent(inEvent);
endVal = streamPairs.size - 1;
forBy (0, endVal, 2) { arg i;
var name = streamPairs[i];
var stream = streamPairs[i+1];
var streamout = stream.next(event);
if (streamout.isNil) { ^inEvent };
if (name.isSequenceableCollection) {
if (name.size > streamout.size) {
("the pattern is not providing enough values to assign to the key set:" + name).warn;
^inEvent
};
name.do { arg key, i;
event.put(key, streamout[i]);
};
}{
event.put(name, streamout);
};
};
^event;
}
recycleDefName {
|event|
var hash, name;
if (defHashLRU.size > maxDefNames) {
hash = defHashLRU.pop();
name = defNames[hash];
defNames[hash] = nil;
defCache[name] = nil;
defNamesFree.add(name);
}
}
*getDefName {
if (defNamesFree.notEmpty) {
^defNamesFree.pop()
} {
defCount = defCount + 1;
^"Pmod_unique_%".format(defCount).asSymbol;
}
}
// Scrub parent event of Pmod-specific values like group - these will disrupt
// the way we set up our groups and heirarchy.
prScrubEvent {
|event|
event[\modGroup] = nil;
^event;
}
// Convert an item from our instrument stream into a SynthDef name.
// This can possible add a new SynthDef if supplied with e.g. a function.
prepareSynth {
|synthVal|
var synthDesc, synthOutput;
^case
{ synthVal.isKindOf(Array) } {
synthVal.collect(this.prepareSynth(_)).reduce({
|a, b|
a.merge(b, {
|a, b|
a.asArray.add(b)
})
})
}
{ synthVal.isKindOf(SimpleNumber) } {
var constRate = rate ?? { \ar }; // default to \ar, because this works for both ar and kr mappings;
var constChannels = channels ?? { 1 };
this.class.wrapSynth(
channels: constChannels, rate: constRate,
defName: "Pmod_constant_%_%".format(constChannels, constRate).asSymbol
).putAll((
value: synthVal
))
}
{ synthVal.isKindOf(Symbol) } {
synthDesc = synthVal.asSynthDesc;
synthOutput = synthDesc.outputs.detect({ |o| o.startingChannel == \out });
if (synthOutput.isNil) {
Error("Synth '%' needs at least one output, connected to an \out synth parameter".format(synthVal)).throw;
};
(
instrument: synthVal,
args: synthDesc.controlNames.flatten.asSet.asArray,
pr_instrumentHash: synthVal.identityHash,
pr_rate: synthOutput.rate.switch(\audio, \ar, \control, \kr),
pr_channels: synthOutput.numberOfChannels
)
}
{ synthVal.isKindOf(AbstractFunction) } {
this.class.wrapSynth(rate, synthVal, channels)
}
{ synthVal.isNil } {
nil
}
{
synthVal.putAll(this.prepareSynth(synthVal[\instrument]));
}
}
asValues {
asValues = true;
}
expand {
^(
Pfunc({
|in|
var thunk;
if (in.isArray) { in = in[0] };
thunk = Thunk({
in.value
});
this.channels.collect {
|i|
{
thunk.value.asArray[i]
}
}
}) <> this
)
}
}
@scztt
Copy link
Author

scztt commented May 11, 2020

(
// very simplified usage....
Pdef(\wonk, Pbind(
	\dur, Prand([Pseq([1/3], 3), 1], inf),
	\amp, 0.8,
	
	// .ar1, .kr1, .ar2, .kr2 etc indicate rate and number of channels (e.g. .ar2 corresponds to
	// a two-element synth arg like: \freq.ar([100, 200])
	\freq, Pmod.kr1(
		// First argument is a function that is compiled to a synth, 
		// OR a synthdef name. It can also be a pattern that returns
		// functons or synthdef names! Synths are re-run when they change,
		// otherwise the same synth continues to modulate.
		{
			|modRate|
			30 + SinOsc.kr(modRate).range(-1, 1)
		}, 
		// Further argument pairs are interpreted like a pbind and passed
		// in to the synth as arguments. 
		\modRate, Prand([2, 1, 6], inf)
		
		// In theory, tru freaks can modulate arguments of a Pmod with
		// another Pmod....
		// Also, you can set your Pmod to multiple arguments by using
		// .expand, e.g.:
		// [\amp, \freq], Pmod.kr2({ [ SinOsc.kr(1).range(440, 540), LFNoise2.kr(1) ] })
	)
)).play;
)

@telephon
Copy link

Excellent!

Perhaps the rate and number of channels could be derived from the return value of the function? Then you wouldn't need the slightly overspecific method names like ar3.

@scztt
Copy link
Author

scztt commented Sep 27, 2020

Yes, this should be possible (and in fact pretty trivial?) - but with some caveats.

I initially implemented with the over-specified methods because I often use it for arrayed synth controls - and the consequences of mis-sending values to an arrayed controls can be quite bad. If you don't send enough, obviously only the first slots in the arrayed control are set (usually this isn't audible, you just have a dead voice or something like this... many hours spent debugging this mistake early on). If you send too many, you'll overwrite other controls, or sometimes simply simple crash the server. The explicit types acted as a sort of assert / explicit cast, between the synth described in the function and the value expected by the specific synth control.

I think that the truly correct implementation would pull the rate and channelization from the parameter being written to, not the synth - this ensures that the event you construct is always correct for the synth you're creating. I tried this at some point - it more or less requires a special version of Pbind that manages the modulator arguments - but there are simply too many Pbind-like classes for this to be feasible. It's no fun if you can't also use it for Pbindf or Pmono, etc.

@telephon
Copy link

Yes, I get the point. This is a bit like it is done in NodeProxy (or ProxySynthDef), but with respec to a proxy not a control.

If you'd require that in the Pmod at that stage the instrument is given already, you could draw this information from the SynthDescLib without much hassle.

@scztt
Copy link
Author

scztt commented Sep 28, 2020

Turns out auto-inferring the channels and rate was pretty easy - just updated, and added some examples as well. This also fixes a handful of cases related to complex multichannel expansion, and a group ordering case I broke at some point.

@telephon
Copy link

Great work.

(two minor simplifications while reading)

sig = \lpf.kr([100, 3000]).collect {
		|lpf, i|
		RLPF.ar(
			sig[i],
			lpf,
			0.6
		)
	};

can be written as:

sig = RLPF.ar(sig, \lpf.kr([100, 3000]), 0.6)
buses[0..(currentSize-1)]

can be written as

buses.keep(currentSize)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment