Skip to content

Instantly share code, notes, and snippets.

@scztt
Last active March 28, 2024 14:25
Show Gist options
  • Save scztt/f9e0fd2e01a21416fb89a97eb944d0b2 to your computer and use it in GitHub Desktop.
Save scztt/f9e0fd2e01a21416fb89a97eb944d0b2 to your computer and use it in GitHub Desktop.
PparStream : Stream {
var <>initStreamAction, <>endStreamAction;
var priorityQ, <now;
var <injectFunc, pausedStreams;
*forkPatterns {
|pattern|
var event, outerEvent, recursionLevel, instrument, embeddingLevel, freq, rest;
var args, defaults, timingOffset, sustain, gatePattern;
var size, newPatterns = [];
if(pattern.notNil) {
// [1] Preserve information from outer pattern, but not delta.
if(~transparency ? true) {
outerEvent = currentEnvironment.copy;
outerEvent.putPairs(
[
\pattern, \type,
\parentType, \addToCleanup,
\removeFromCleanup, \sustain,
\legato, \timingOffset, \delta, \gatePattern
].collect([_, nil]).flatten
)
} {
outerEvent = Event.default.copy;
};
// [2] If a function, fill in it's args from the environment
if (pattern.isKindOf(Function)) {
defaults = pattern.def.prototypeFrame;
args = pattern.def.argNames.collect {
|name, i|
currentEnvironment[name].value ?? { defaults[i] }
};
pattern = pattern.value(*args);
};
// [3] Grab timing properties
sustain = ~sustain.value;
~timingOffset = timingOffset = (~timingOffset.value ? 0);
~gatePattern = gatePattern = (~gatePattern.value ? true);
// [4] Apply flop
if (~flop ? false) {
outerEvent = outerEvent.asPairs.flop.collect(_.asEvent);
outerEvent.do(_.parent_(currentEnvironment.parent));
} {
outerEvent = [outerEvent]
};
// [5] Process inner pattern
outerEvent.do {
|outerEvent, i|
var innerPattern;
outerEvent.use {
innerPattern = pattern;
if (innerPattern.isKindOf(Symbol)) {
innerPattern = Pdef(pattern);
};
if (innerPattern.isKindOf(Stream) and: { outerEvent.size > 1 }) {
innerPattern = innerPattern.copy;
};
if (innerPattern.isKindOf(Event)) {
Error("Event patterns must be wrapped in a Ref or a function when passed in as an \\instrument argument").throw;
};
if (innerPattern.isKindOf(PatternProxy)) {
innerPattern = innerPattern.pattern; // optimization. outer pattern takes care for replacement
};
if (innerPattern.notNil) {
// not sure why we DON'T need to account for positive timingOffset here,
// but if we do it breaks....
if (gatePattern) {
innerPattern = innerPattern.finDur(sustain - timingOffset.min(0) - 0.000000001);
};
if (not(~filter ? false)) {
innerPattern = Pevent(innerPattern, outerEvent).asStream;
};
if (timingOffset < 0) {
innerPattern.fastForward(timingOffset.neg, 0, outerEvent);
};
newPatterns = newPatterns.add([
timingOffset, innerPattern
]);
}
}
}
};
^newPatterns
}
*initClass {
Class.initClassTree(Event);
Event.addEventType(\fork, {
this.forkPatterns(~pattern ?? { ~instrument }).do {
|newPatterns|
~injectStream.value(
delta: newPatterns[0],
stream: newPatterns[1],
inval: newPatterns[2]
)
}
}, Event.parentEvents.default.copy.putAll((legato:1)));
}
*new {
^super.new.init
}
init {
priorityQ = PriorityQueue();
pausedStreams = IdentitySet();
now = 0;
injectFunc = { |delta, stream, inval| this.injectStream(delta, stream, inval) };
}
reset {
this.init();
}
injectStream {
|delta, stream, inval|
stream = stream.asStream;
// "injecting stream %".format(stream.identityHash).postln;
initStreamAction !? {
stream = initStreamAction.value(stream, inval).asStream
};
priorityQ.put(now + delta, stream);
^stream;
}
pauseStream {
|stream|
pausedStreams = pausedStreams.add(stream);
}
resumeStream {
|stream, delta=0|
pausedStreams = pausedStreams.remove(stream);
}
injectEvent {
|delta, event, inval|
this.injectStream(
delta,
Routine({ event.yield; nil.yield }, 2),
inval
)
}
// processStreamsToPause {
// streamsToPause.do {
// |stream|
// priorityQ.remove(stream);
// pausedStreams = pausedStreams.add(stream);
// };
// streamsToPause.clear();
// }
embedInStream {
|inval, duration|
var stream, nextTime, outval, endTime, nextTimeDelta;
endTime = now + (duration ? inf);
while { priorityQ.notEmpty and: { now < endTime } } {
nextTime = min(
priorityQ.topPriority,
endTime
);
// "now: % nextTime: % endTime: %".format(now, nextTime, endTime).postln;
if (nextTime > now) {
nextTimeDelta = nextTime - now;
pausedStreams.do {
|stream|
priorityQ.put(nextTimeDelta, stream);
};
inval = Event.silent(nextTimeDelta).yield;
now = nextTime;
} {
stream = priorityQ.pop;
if (pausedStreams.includes(stream).not) {
outval = stream.next(inval).asEvent;
if (outval.notNil) {
// requeue stream
priorityQ.put(now + outval.delta, stream);
outval[\delta] = 0;
outval[\injectStream] = outval[\injectStream] ?? { injectFunc };
inval = outval.yield;
} {
endStreamAction.value(stream, inval);
}
};
}
};
if (duration.notNil and: { now < endTime }) {
inval = Event.silent(endTime - now).put(\debug, "PparStream(%):embedInStream:207".format(this.identityHash)).yield;
};
^inval
}
}
Ppar2 : ListPattern {
var <>initStreamAction, <>endStreamAction;
embedInStream {
|inval|
var parStream = PparStream()
.initStreamAction_(initStreamAction)
.endStreamAction_(endStreamAction);
repeats.value(inval).do({
list.do(parStream.injectStream(0, _, inval));
parStream.embedInStream(inval);
parStream.reset();
})
}
}
PwithStream : Stream {
var <>parStream, <>inputStream, <>replacementStream;
var routine;
// *new {
// |parStream, inputStream, condition, patternFunction, replace, gate, insert|
// ^super.newCopyArgs(parStream, inputStream, condition, patternFunction, replace, gate, insert).init
// }
*new {
|parStream, inputStream, replacementStream|
^super.newCopyArgs(parStream, inputStream, replacementStream).init
}
init {
routine = Routine({
|inEvent|
this.embedInStream(inEvent);
});
}
next {
|inEvent|
^routine.next(inEvent);
}
embedInStream {
|inEvent|
var originalEventDelta;
var inEvents, outputStream, eventFilters;
outputStream = Routine({
|inEvent|
var shouldPlayOriginalEvent;
var shouldWaitForForkedStream;
var shouldReplaceAll;
var shouldGatePattern;
var shouldFilterEvents;
var forkPatterns=[], insertPatterns=[], resultPatterns, inputFilters;
var outEvent, outDelta, outReplacement, skipRests = false;
while {
while {
outEvent = inputStream.next(inEvent);
skipRests and: { outEvent.isRest }
};
skipRests = false;
outEvent.notNil
} {
shouldPlayOriginalEvent = true;
shouldWaitForForkedStream = false;
shouldReplaceAll = false;
shouldGatePattern = nil;
shouldFilterEvents = false;
if (outEvent.isRest.not and: {
outEvent.use {
/////////////////////////////////////////////////////////////////////////////////
outReplacement = replacementStream.next(outEvent);
/////////////////////////////////////////////////////////////////////////////////
};
outReplacement.notNil
}) {
shouldPlayOriginalEvent = false;
shouldWaitForForkedStream = false;
shouldReplaceAll = false;
outEvent.copy.use {
// SUBTLE: We want to prevent gating if we're doing an insert,
// but we only know if we're doing an insert once we've run our function.
shouldGatePattern = ~gatePattern ?? { true };
~gatePattern = {
if (~insert !? _.value ?? false) {
false
} {
shouldGatePattern.value
}
};
/////////////////////////////////////////////////////////////////////////////////
resultPatterns = PparStream.forkPatterns(outReplacement);
/////////////////////////////////////////////////////////////////////////////////
shouldFilterEvents = ~filter ?? { shouldFilterEvents };
shouldWaitForForkedStream = ~insert ?? { shouldWaitForForkedStream };
shouldReplaceAll = ~replaceAll ?? { shouldWaitForForkedStream };
shouldPlayOriginalEvent = (
resultPatterns.isEmpty
or: { shouldFilterEvents }
// or: { shouldWaitForForkedStream.postln }
or: {
~replace !? _.not ?? { shouldPlayOriginalEvent }
}
);
if (shouldFilterEvents) {
resultPatterns.do {
|pattern|
inputFilters = inputFilters.add(pattern[1].asStream);
}
};
if (resultPatterns.isEmpty.not && shouldReplaceAll) {
forkPatterns.extend(0);
insertPatterns.extend(0);
};
if (shouldFilterEvents.not) {
if (shouldWaitForForkedStream) {
insertPatterns = insertPatterns.addAll(resultPatterns)
} {
forkPatterns = forkPatterns.addAll(resultPatterns)
}
}
};
};
//////////////////////////////////////////////////////////////////////////////////////////
inputFilters.copy.do {
|filter|
var newEvent = filter.next(outEvent);
if (newEvent.isNil) {
inputFilters.remove(filter)
} {
outEvent = newEvent;
}
};
//////////////////////////////////////////////////////////////////////////////////////////
// Process all input events at the current time BEFORE forking / inserting
if (outEvent.delta > 0) {
// If we generated no works, then yield straight through
// First, inject our forked streams
forkPatterns !? {
|toFork|
toFork = toFork.copy();
forkPatterns.extend(0);
toFork.do {
|p|
// "parStream.injectStream(%, %, %);".format(p[0], p[1], outEvent).postln;
parStream.injectStream(p[0], p[1], outEvent);
}
};
if (insertPatterns.size > 0) {
insertPatterns !? {
|toInsert|
var oldDelta;
// TODO: This means the next event occurs RIGHT after this insert ends - do we want this?
if (shouldPlayOriginalEvent) {
// // @SUBTLE - If we're replacing the original event, then the overall duration of our inserted
// // pattern is the "new" duration.
// oldDelta = outEvent.delta;
// toInsert = toInsert.add([
// 0,
// Event.rest(oldDelta)
// ]);
"playing original event through insert".postln;
inEvent = outEvent.copy.put(\delta, 0).put(\debug, 387).yield;
};
toInsert = toInsert.flatten;
insertPatterns.extend(0);
inEvent = Ptpar(toInsert).trace.embedInStream(outEvent);
skipRests = true;
};
} {
if (shouldPlayOriginalEvent) {
inEvent = outEvent.yield;
} {
inEvent = Event.silent(outEvent.delta).put(\debug, 406).yield
}
}
} {
if (shouldPlayOriginalEvent) {
inEvent = outEvent.yield;
} {
inEvent = Event.silent(outEvent.delta).put(\debug, 413).yield
}
}
}
});
parStream.injectStream(0, outputStream, inEvent);
^parStream.embedInStream(inEvent)
}
}
Pwith : Pattern {
// var <inputPattern, <conditions, <patternFunctions,
var inputPattern, replacements, <>replace, <>gate, <>insert;
*new {
|inputPattern ...replacements|
^super.newCopyArgs(inputPattern, replacements ?? {[]})
}
*conditions {
|inputPattern, condition, pattern ...conditionPatterns|
conditionPatterns = ([condition, pattern] ++ conditionPatterns).clump(2);
conditionPatterns = conditionPatterns.collect {
|condPat|
Pfunc({
|e|
if (condPat[0].value(e)) {
condPat[1].value(e)
} {
nil
}
})
};
^this.new(
inputPattern,
*conditionPatterns
).init
}
init {
replace = true;
gate = false;
insert = false;
}
fixPattern {
|p|
^p.isFunction.if({
Pfunc({ |e| e.use(p) })
}, {
p
})
}
asStream {
var stream;
var inputStream = inputPattern.asStream;
replacements.do {
|replacement|
inputStream = PwithStream(
parStream: PparStream(),
inputStream: inputStream,
replacementStream: this.fixPattern(replacement).asPattern.asStream,
);
}
// [conditions, patternFunctions].flop.flatten.pairsDo {
// |condition, patternFunction|
//
// inputStream = PwithStream(
// PparStream(),
// inputStream,
// condition,
// this.fixPattern(patternFunction),
// replace,
// gate,
// insert: false
// );
// };
^inputStream
}
}
+Ppar {
// *new {
// |list, repeats=1|
// ^Ppar2.new(list, repeats);
// }
}
(
Pdef(\input, Pbind(
\dur, 2, \legato, 1,
\octave, 3, \scale, Scale.kurd,
\degree, Pseq([0, 1, 7, 3, 5], inf)
));
)
(
Pdef(\with, Pwith(
Pdef(\input),
// Each additional argument is a function that is called for every event
// If the function returns a non-nil value, it is treated as a pattern and replaces
// the input event, with some variation to what "replace" means.
{
// randomly replace with a pattern 50% of the time
if ([true, false].choose) {
Pbind(
\dur, 1/4,
\octave, 4,
\degree, Pseq([-4, 3, 1, 2], inf)
)
}
}
).trace).play;
)
(
Pdef(\with, Pwith(
Pdef(\input),
{
// If a function is provided, that function is evaluated once for each incoming event,
// with event keys filling in function arguments (same as recursive phrasing).
// Note the slightly weird function-within-a-function syntax (because the outer
// function is for deciding WHETHER to replace and e.g. returning nil).
{
|dur, degree|
// Replace single events with a faster arpeggio
Pbind(
\dur, dur / 10,
\degree, degree + Pseq([-4, 3, 1, 2], inf)
)
}
}
).trace).play;
)
(
// Pdef(\with).clear;
Pdef(\with, Pwith(
// Replacement patterns follow the timing (\dur, \sustain) of the incoming note -
// for example, with a shorter sustain/legato, patterns will be clipped to a shorter
// length - with a longer legato, patterns will extend and overlap following the
// incoming events.
Pbind(
\legato, 2
) <> Pdef(\input),
{{
|dur, degree|
Pbind(
\dur, dur / 8,
\degree, degree + Pseq([-1, 4, -8], inf)
)
}}
).trace).play;
)
(
// Pdef(\with).clear;
Pdef(\with, Pwith(
// Replacement patterns follow the timing (\dur, \sustain) of the incoming note -
// for example, with a shorter sustain/legato, patterns will be clipped to a shorter
// length - with a longer legato, patterns will extend and overlap following the
// incoming events.
Pbind(
\legato, 2
) <> Pdef(\input),
{{
|dur, degree|
Pbind(
\dur, dur / 8,
\degree, degree + Pseq([-1, 4, -8], inf)
)
}}
).trace).play;
)
(
// Pdef(\with).clear;
Pdef(\with, Pwith(
Pbind(
\stepSize, 3
) <> Pdef(\input),
// Outer event keys are visible in the context of your pattern via Pkey,
// which is an alternative to using function args to pick these up.
{
Pbind(
\dur, Pkey(\dur) / 8,
\degree, Pkey(\degree) + (Pkey(\stepSize) * Pseq([-1, 2, -3], inf))
)
}
).trace).play;
)
(
// Pdef(\with).clear;
Pdef(\with, Pwith(
// Outer events have several keys that control how pattern replacement happens....
// All option keys, plus their default values.
Pbind(
// Replacement patterns should be gated to the \sustain duration of the incoming event.
// If `false`, replacement patterns can play as long as they want - be careful, if the patterns
// play forever then you'll just stack up infinitely-playing patterns indefinitely!
\gatePattern, true,
// If `true`, generate new patterns based on multichannel expansion of the input event.
// For example, Pbind(\flop, true, \degree, [1, 2, 3]) would produce THREE replacement patterns,
// one for each degree (by default, only one is generated and \degree will be visible as an array).
\flop, false,
// If `true`, replace the input event with the pattern.
// If `false`, play the original event AND the pattern together.
\replace, true,
// If `true` AND several input events occur at the same time (e.g. from a Ppar), then
// only the LAST event is replaced (e.g subsequent events at the same time replace old patterns)
// If `faise`, each input event generates a new pattern.
\replaceAll, false,
// If `false`, timing (dur/sustain) follows the input event, e.g. the overall timing
// of input events will not be changed.
// If `true`, replacement pattern will be "inserted", e.g. it will play until it's finished,
// before the next input event is processed. Note that this can occasionally have some
// unexpected results, which may be bugs or may simply be theoreticall correct-but-strange behavior....
\insert, false,
// If `true`, events from the replacement pattern will replace ALL events for as long as the replacement
// event is running. The idea here is you can have a replacement like Pbind(\octave, Pkey(\octave).finDur(6) + 2),
// and it will raise the octave of all input events by 2 for 6 beats. Note that this ONLY really makes
// sense with (\gatePattern, false)
\filter, false,
) <> Pdef(\input),
{
// note that these keys can also be set inside the replacement function like this,
// so you don't have to pollute your outer pattern with unrelated things.
~gatePattern = false;
Pbind(
\dur, Pkey(\dur) / 8,
\degree, Pkey(\degree) + (Pkey(\stepSize) * Pseq([-1, 2, -3], inf))
)
}
).trace).play;
)
(
// limit pattern to 2 beats regardless of outer event
Pdef(\with, Pwith(
Pbind(
\dur, Prand([1, 1.5, 2], inf),
\octave, Pxrand([3, 4, 5], inf)
) <> Pdef(\input),
{
~gatePattern = false;
Pbind(
\dur, Pconst(2, 1/6),
\degree, Pkey(\degree) + Pseq([-1, 2, -3], inf)
)
}
).trace).play;
)
(
// Don't replace, meaning play original events AND replacement pattern
Pdef(\with, Pwith(
Pdef(\input),
{
~replace = false;
Pbind(
\octave, 5,
\amp, 0.03,
\dur, 1 / 6,
\degree, Pkey(\degree) + Pseq([-1, 2, -3], inf)
)
}
).trace).play;
)
(
// insert, meaning ignore the input events duration and play the entire replacement Pbind
// until its finished - here we randomly set the number of values pulled from the Pser to
// limit it.
Pdef(\with, Pwith(
Pdef(\input),
{
~insert = true;
Pbind(
\octave, [4, 5].choose,
\dur, 1 / 12,
\degree, Pkey(\degree) + Pser([-1, 2, -3, 2, -1], rrand(4, 24))
)
}
).trace).play;
)
(
// For 1/4 events (see our .coin condition), we will add a filter that modifies
// the octave of all incoming events according to `octBoost`, for `duration` beats.
// Note that we can potentially have multiple filters going at once - they are applied
// in the order they were created (in our case, multiple octave modifiers can be applied).
Pdef(\with, Pwith(
Pbind(\dur, 1/4) <> Pdef(\input),
{
var octBoost, duration;
if (0.25.coin) {
~filter = true; ~gatePattern = false;
octBoost = [-1, 1, 2].choose;
duration = [3, 4, 6].choose;
"boosting octave by % for % beats".format(octBoost, duration).postln;
Pbind(
\octave, (Pkey(\octave) + octBoost).wrap(2, 7).trace,
\dummy, Pfindur(duration, 0)
)
}
}
)).play;
)
(
Pdef(\with, Pwith(
Pbind(\dur, 1/4) <> Pdef(\input),
{
var octBoost, duration;
if (0.25.coin) {
~filter = true; ~gatePattern = false;
octBoost = [-1, 1, 2].choose;
duration = [3, 4, 6].choose;
"boosting octave by % for % beats".format(octBoost, duration).postln;
Pbind(
\octave, Pkey(\octave) + octBoost,
\dummy, Pfindur(duration, 0)
)
}
}
).trace).play;
)
(
// If more than one replacement is provided, theyre simply chained such that e.g.:
// Pwith(input, replacement1, replacement2)
// is equivalent to:
// Pwith(Pwith(input, replacement1), replacement2)
Pdef(\with, Pwith(
// 1 event every 4 beats, provide a list of degrees
Pbind(
\dur, Prand([1, 2, 4] / 1.5, inf), \legato, 1,
\scale, Scale.hexSus,
\chord, Pseq([
[-1, 2, 4, 5],
[-2, 2, 4, 5] - 1,
], inf)
),
// replace with a arp-y pattern based on the chord
{{
|chord|
Pbind(
\dur, 1 / 6,
\degree, Pseq(chord, inf),
\octave, 3,
)
}},
// Occasionally replace our event with a slowed down ascending
// series of events.
{
0.05.coin.if {
~gatePattern = false;
Pbind(
\dur, Pser([1 / 0.75], 6),
\amp, 0.1,
\octave, Pkey(\octave) + 1 + Pseries()
)
}
}
).trace).play;
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment