Skip to content

Instantly share code, notes, and snippets.

@scztt
Last active July 4, 2024 13:39
Show Gist options
  • 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 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