Event Pattern to scramble events

Hi there, I’m trying to write a Pattern that will scramble Events (reorder them in time). Of course, this needs to be “look ahead”, so I do like Pfindur and ask for events up to a specified duration, store them, then scramble them and return.

I’ve got it mostly working, but it fails when I afterwards combine the scrambled events with other patterns (e.g. chaining).

Here’s an example of what I’ve got:

// (see definition of Pscramble and Pattern.scramble below)

// Works as expected (random Event order)
p = Pbind(\degree, Pseq((0..3)), \dur, 0.25, \amp, Pseq((1..4) / 4)); // degree and amp Pbind
p.scramble.asStream.nextN(4, ()).do(_.postln)
//   ( 'degree': 0, 'dur': 0.25, 'amp': 0.25 )
//   ( 'degree': 3, 'dur': 0.25, 'amp': 1.0 )
//   ( 'degree': 1, 'dur': 0.25, 'amp': 0.5 )
//   ( 'degree': 2, 'dur': 0.25, 'amp': 0.75 )

// Also works as expected with Pchain (scrambling after the chaining)
d = Pbind(\degree, Pseq((0..3)), \dur, 0.25); // degree Pbind
a = Pbind(\amp, Pseq((1..4) / 4), \dur, 0.25); // amp Pbind
(d <> a).scramble.asStream.nextN(4, ()).do(_.postln)
//   ( 'degree': 1, 'dur': 0.25, 'amp': 0.5 )
//   ( 'degree': 2, 'dur': 0.25, 'amp': 0.75 )
//   ( 'degree': 0, 'dur': 0.25, 'amp': 0.25 )
//   ( 'degree': 3, 'dur': 0.25, 'amp': 1.0 )

// Doesn't work when you scramble and then Pchain, it's not
// taking events from the chained stream (all amps are the same).
// (would want/expect a random degree followed by a sequential order of amps: 0.25,0.5,0.75,1)
(d.scramble <> a).asStream.nextN(4, ()).do(_.postln)
//   ( 'degree': 0, 'dur': 0.25, 'amp': 0.25 )
//   ( 'degree': 3, 'dur': 0.25, 'amp': 0.25 )
//   ( 'degree': 2, 'dur': 0.25, 'amp': 0.25 )
//   ( 'degree': 1, 'dur': 0.25, 'amp': 0.25 )

If anyone is willing to take a look, here is my work in progress on Pscramble (I’m not very familiar with Patterns and Streams, so any suggestions/critiques are welcome):

// Pscramble.sc

// Event Pattern that takes a duration of elements and looks ahead to scramble the result
Pscramble : FilterPattern {
	var <>dur;
	var <>randSeed;
	const tolerance = 0.001;

	*new { arg pattern, dur = 1, randSeed;
		^super.new(pattern).dur_(dur).randSeed_(randSeed)
	}

	storeArgs { ^[pattern,dur,randSeed] }

	prReturnEvents { arg allEvents, seed;
		var lastEvent;

		// Scramble and yield all the Events in our list before exiting
		"Scrambling % Events".format(allEvents.size).postln;

		if (seed.notNil) {
			"Setting seed to %".format(seed).postln;
			thisThread.randSeed = seed;
		};
		allEvents = allEvents.scramble;
		allEvents.do{ arg ev; lastEvent = yield(ev.debug("yield")) }
		^lastEvent
	}

	embedInStream { arg event;
		var delta, elapsed = 0.0, nextElapsed, inevent;
		var localdur = dur.value(event);
		var stream = pattern.asStream;
		var seedStream = randSeed.asStream;
		var cleanup = EventStreamCleanup.new;
		var allEvents = [];
		loop {
			inevent = stream.next(event).asEvent ?? {
				^cleanup.exit(this.prReturnEvents(allEvents, seedStream.next(event)).debug("returning after playing loop"));
			};
			cleanup.update(inevent);
			delta = inevent.delta;
			nextElapsed = elapsed + delta;
			if (nextElapsed.roundUp(tolerance) >= localdur) {
				// must always copy an event before altering it.
				// fix remaining time and yield all the events.
				inevent = inevent.copy.put(\dur, localdur - elapsed);
				"Adding last: % (elapsed %)".format(inevent, localdur).postln;
				allEvents = allEvents.add(inevent);
				^cleanup.exit(this.prReturnEvents(allEvents, seedStream.next(event))).debug("returning after dur");
			};

			elapsed = nextElapsed;
			"Adding % (elapsed %)".format(inevent, elapsed).postln;
			allEvents = allEvents.add(inevent.copy);
			event = inevent;
		}
	}
}

+Pattern {
	scramble { arg dur = 1, randSeed;
		^Pscramble(this, dur, randSeed)
	}
}

Thanks,
Glen.

1 Like

For the record, here’s my “finished” (for now) version of this class. It’s pretty cool…and seems to work as I’d want in the cases I tested above (as well as a few others)…though I’m sure there may be certain Pattern cases (e.g. Ppar or things requiring stream cleanup) where it doesn’t work properly.

If you use it with Pseed (or with my Pattern.scramble overload that has a randSeed argument), you can get repeatable scrambled patterns, which is great for live coding.

(Not sure what the storeArgs is for, but I copied it from another example… :wink: )

// Event Pattern that takes a duration and scans ahead and
// scrambles the order of all Events that fall into that time
//   by Glen Fraser

Pscramble : FilterPattern {
	var <>dur;
	const tolerance = 0.001;

	*new { arg pattern, dur = 1;
		^super.new(pattern).dur_(dur);
	}

	storeArgs { ^[pattern,dur] }

	prReturnEvents { arg allEvents, inEvent;
		// Scramble and yield all the Events in our list before exiting
		allEvents = allEvents.scramble;
		allEvents.do{ arg ev, index;
			inEvent = ev.next(inEvent).yield
		};
		^inEvent
	}

	embedInStream { arg inEvent;
		var delta, elapsed = 0.0, nextElapsed, outEvent;
		var localdur = dur.value(inEvent);
		var stream = pattern.asStream;
		var cleanup = EventStreamCleanup.new;
		var allEvents = [];
		var originalInEvent = inEvent;
		inEvent = (); // use a "dummy" Event for the initial skipping-forward part of our operation
		loop {
			outEvent = stream.next(inEvent).asEvent;
			if (outEvent.isNil) {
				^cleanup.exit(this.prReturnEvents(allEvents, originalInEvent))
			};

			cleanup.update(originalInEvent);
			delta = outEvent.delta;
			nextElapsed = elapsed + delta;
			if (nextElapsed.roundUp(tolerance) >= localdur) {
				// must copy an event before altering it.
				// fix remaining time and yield all the events.
				outEvent = outEvent.copy.put(\dur, localdur - elapsed);
				allEvents = allEvents.add(outEvent);
				^cleanup.exit(this.prReturnEvents(allEvents, originalInEvent));
			};

			elapsed = nextElapsed;
			allEvents = allEvents.add(outEvent);
			inEvent = outEvent;
		}
	}
}

+Pattern {

	scramble { arg dur = 1, randSeed;
		var p = Pscramble(this, dur);
		^if (randSeed.notNil) { Pseed(Pn(randSeed, 1), p) } { p }
	}

}

I will resolve my topic, but if anyone has any suggestions or comments, I’d be glad to hear them.

2 Likes

Looks generally good! Probably incompatible with Pmono(Artic).

In general the pattern system assumes data will be generated at the time it’s needed (1:1 correspondence between next's input and output). I’ve struggled myself with patterns like this one that need to “run ahead.” I think I see what you’re doing but I’m not sure of every step.

Couple of thoughts:

  • The inEvent may contain information that the user wants to be available to the new events. inEvent = () will disallow that usage. Maybe it could be outEvent = stream.next(inEvent.copy).asEvent; (so the original inEvent is never modified but the data are passed in).

  • cleanup.update(outEvent); I believe; there’s no reason to update the cleanup repeatedly for an event that has no new information.

  • inEvent = ev.next(inEvent).yield – “next” is confusing to read because it implies a sequence, but it’s applied to an isolated object. It’s really a synonym for composeEvents so for clarity, why not inEvent = inEvent.composeEvents(ev).yield (be explicit about the real meaning)?

  • ^cleanup.exit(this.prReturnEvents(allEvents, originalInEvent)); – I would definitely split this onto two lines – inEvent = this.prReturnEvents(allEvents, originalInEvent); then ^cleanup.exit(inEvent);. Returning events is the most critical part of the flow! Burying it as an argument to a less critical method hurts readability. I seriously almost overlooked it.

  • inEvent = outEvent; – I wouldn’t do this. Data from the previous event may leak into the next (for instance, if someone uses a Pfunc to add data to a specific key conditionally, but not every time).

Hope that helps.
hjh

Thanks for looking at this, James, and for your comments! Sorry for the delay replying, but I had some performances to get ready for, which are now done… (-;

I’ve cleaned up a few of the code things you mention. However, your first idea about copying the input event rather than using my empty “dummy” event while caching the events up to the given duration causes problems when I do chaining after running Pscramble. For example, this example works with my version, but not your suggested change:

d = Pbind(\degree, Pseq((0..3)), \dur, 0.25); // increasing degrees
a = Pbind(\amp, Pseq((1..4) / 4), \dur, 0.25); // increasing amps

// Works in both cases...scrambled degrees and "matching" amps
(d <> a).scramble(randSeed: 678).asStream.nextN(4, ()).do(_.postln);
// ( 'degree': 2, 'dur': 0.25, 'amp': 0.75 )
// ( 'degree': 3, 'dur': 0.25, 'amp': 1.0 )
// ( 'degree': 1, 'dur': 0.25, 'amp': 0.5 )
// ( 'degree': 0, 'dur': 0.25, 'amp': 0.25 )

// Doesn't work with James' suggestion: amps are stuck at 0.25
(d.scramble(randSeed: 678) <> a).asStream.nextN(4, ()).do(_.postln);
// ( 'degree': 2, 'dur': 0.25, 'amp': 0.25 )
// ( 'degree': 3, 'dur': 0.25, 'amp': 0.25 )
// ( 'degree': 1, 'dur': 0.25, 'amp': 0.25 )
// ( 'degree': 0, 'dur': 0.25, 'amp': 0.25 )

I recognize that Pscramble(*) won’t/can’t work in all cases. For example, with EventStreamCleanup (Pmono) and Ppar it definitely fails, and probably when you pass in some events using Pfunc (can you give me an example of what you said: “if someone uses a Pfunc to add data to a specific key conditionally”?). I’d like for it to be able to handle as much as possible.

Thanks again,
Glen.

(*) I’ve since generalized Pscramble to be Parrop so it can apply any Array operation on the time-collected Events, for example: mirror, pyramid, reverse, as well as the original scramble.

OK, I see what you mean.

Normally, it would be correct to preserve information coming from the inEvent. But this is an unusual case, “prefetching” data from the pattern to be scrambled. So, I have to admit that in this case, the dummy event inEvent = () is better.

If you restore that line (only that line), then you get the behavior that you expected.

I still have my doubts about inEvent = outEvent at the bottom of the loop. Here is a case where it breaks:

// here we'll do something unusual:
// conditionally populate a key
d = Pbind(\degree, Pseq((0..3)), \dur, 0.25).collect { |ev|
	if(ev[\degree].odd) { ev[\odd] = true };
	ev;
};
a = Pbind(\amp, Pseq((1..4) / 4), \dur, 0.25);

// *with* 'inEvent = outEvent'
(d.scramble(randSeed: 678) <> a).asStream.nextN(4, ()).do(_.postln);
( 'degree': 2, 'dur': 0.25, 'odd': true, 'amp': 0.25 )
( 'degree': 3, 'dur': 0.25, 'odd': true, 'amp': 0.5 )
( 'degree': 1, 'dur': 0.25, 'odd': true, 'amp': 0.75 )
( 'degree': 0, 'dur': 0.25, 'amp': 1.0 )

'degree': 2, ... 'odd': true is incorrect. Because of replacing the inEvent with output data, once the conditionally-set key gets a value, then it’s never un-set.

But if you don’t replace, and always go back to the dummy event, then it’s fine:

// without 'inEvent = outEvent'
( 'degree': 2, 'dur': 0.25, 'amp': 0.25 )  <<-- now OK
( 'degree': 3, 'dur': 0.25, 'odd': true, 'amp': 0.5 )
( 'degree': 1, 'dur': 0.25, 'odd': true, 'amp': 0.75 )
( 'degree': 0, 'dur': 0.25, 'amp': 1.0 )

hjh

Thanks, yes, after your earlier mail I’d removed the setting of inEvent = outEvent. Thanks for the example showing precisely why it shouldn’t be there; I’ve added your case to my unit tests! (-;

I spent some time today looking at handling Ppar (Events with delta=0), but I think I’ll leave it (for now at least). I don’t need this functionality at the moment, though it would be nice. It’s quite hard to say what it should do in these cases…If you want to reverse or scramble a set of events, you might “naïvely” assume you’d want to do it by their onset time (so events starting at the same time would all start together after reversing/scrambling). However, this becomes quite complicated when (parallel) things have different durations. For example:

p = Pbind(\degree, Pseq((0..3) ++ Rest()), \dur, Pseq([0.5, 0.25, 0.125, 0.125, 1]));
q = Pbind(\degree, Pseq((6..9)), \dur, Pseq([0.25,0.5,0.5,0.75]));

// |0___1_23|r_______|
// |6_7___8_|__9_____|

// (onset time -> Event), separated into common onset times
a = [
	[
		(0.0 -> ( 'degree': 0, 'delta': 0.0, 'dur': 0.5 )),
		(0.0 -> ( 'degree': 6, 'delta': 0.25, 'dur': 0.25 ))
	],
	(0.25 -> ( 'degree': 7, 'delta': 0.25, 'dur': 0.5 )),
	(0.5 -> ( 'degree': 1, 'delta': 0.25, 'dur': 0.25 )),
	[
		(0.75 -> ( 'degree': 8, 'delta': 0.0, 'dur': 0.5 )),
		(0.75 -> ( 'degree': 2, 'delta': 0.125, 'dur': 0.125 ))
	],
	(0.875 -> ( 'degree': 3, 'delta': 0.125, 'dur': 0.125 )),
	(1.0 -> ( 'degree': Rest(1), 'delta': 0.25, 'dur': 1 )),
	(1.25 -> ( 'degree': 9, 'delta': 0.75, 'dur': 0.75 )),
	(2.0 -> ( 'delta': 0.0, 'dur': Rest(0.0) ))
];

For reversing, intuitively one can imagine the result “should” be:

// |r_______|321_0___|
// |9_____8_|__7___6_|

a = [
	[
		(0.0 -> ( 'delta': 0.0, 'degree': Rest(1), 'dur': 1 )),
		(0.0 -> ( 'delta': 0.75, 'degree': 9, 'dur': 0.75 ))
	],
	(0.75 -> ( 'delta': 0.25, 'degree': 8, 'dur': 0.5 )),
	(1.0 -> ( 'delta': 0.125, 'degree': 3, 'dur': 0.125 )),
	(1.125 -> ( 'delta': 0.125, 'degree': 2, 'dur': 0.125 )),
	[
		(1.25 -> ( 'delta': 0.0, 'degree': 7, 'dur': 0.5 )),
		(1.25 -> ( 'delta': 0.25, 'degree': 1, 'dur': 0.25 ))
	],
	(1.5 -> ( 'delta': 0.25, 'degree': 0, 'dur': 0.5 )),
	(1.75 -> ( 'delta': 0.25, 'degree': 6, 'dur': 0.25 )),
	(2.0 -> ( 'delta': 0.0, 'dur': Rest(0.0) ))
];

Figuring out the order of events and their deltas is not trivial, you can’t just separate the original events by onset time (in the forward direction), reverse those, then flatten and play back the Events…because when the event is going backwards its end time becomes its beginning, and events that started at the same time don’t necessarily end at the same time.

This could be handled as a special case for reverse, but in the general case (scramble, perfectShuffle, mirror, etc.) you don’t know whether any given event is going forwards or backwards.

Anyhow, it’s an interesting thought experiment…( for now :wink: )