Last active
January 27, 2025 14:12
-
-
Save scztt/51e19767ae954adfa8a5783dce183209 to your computer and use it in GitHub Desktop.
Pmod.sc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
( | |
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 | |
) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Great work.
(two minor simplifications while reading)
can be written as:
can be written as