Tips for organising polyphonic compositions?

For a “fixed enough” composition, what I do to “move around” section is change the timingOffset of all the events in the track/instrument to move. E.g.

c = Pbind(*[dur: 0.3, degree: Pseries(0, 1, 8)])
d = Pbind(*[dur: 0.3, ctranspose: 12, degree: Pseries(0, 1, 8)])

Ppar([c, d]).play // in sync

Ppar([c, d <> (timingOffset: 0.9)]).play // 2nd one delayed 3 "beats"
Ppar([c, d <> (timingOffset: 0.1)]).play // 2nd one delayed "a little"

Ptpar basically does the same, but you pass the offsets interleaved with the patterns:

Ptpar([0, c, 0.9, d]).play 
Ptpar([0, c, 0.1, d]).play 

ScTimeLine (linked in a post above) basically uses this in combo with Pfin or Pfindur to also limit how long a “track” plays, e.g.

Ptpar([0, c, 0.9, Pfin(3, d)]).play 

There’s a more complicated way I do this to for things “cued” symbolically from other events… i.e. timingOffset can be calculated if the durations are known in advance. But let’s say durs are random

// riddle me a sync on this, dur not known until it plays!
c = Pbind(*[dur: Pfunc { 0.1 * rrand(1, 3) }, degree: Pseries(0, 1, 8)])

// a basic idea (a bit like Spawner), but this won't change durs in d
(c <> Pbind(*[cnt: Pseries(0, 1), callback: { if(~cnt == 3) {d.play} } ])).play

// pull events from d and replace durs with what c is at
// this will not fully "consume" d though
(
q = d.asStream;
(c <> Pbind(*[cnt: Pseries(0, 1),
	callback: { if(~cnt >= 3) { (q.next((amp:0.05))[\dur] = ~dur).play } } ])).play
)

You could make that more encapsulated with a Plazy. Also, combine with previous (fixed offset) idea, and/or also use Pdrop to skip some beats from the “slave” pattern etc.

(Plazy({
	var narr = [7, 4, 1];
	Pn(Plazy({
		var n = narr.pop;
		var q = Pdrop(n, d <> (amp: 0.05, timingOffset: 0.04)).asStream;
		(c <> Pbind(*[cnt: Pseries(0, 1),
			callback: { if(~cnt >= n) { (q.next(())[\dur] = ~dur).play } } ]))
	}), narr.size)
}).play)

Another way to trigger streams is with Pgate, but you have use a “fake” first event in a stream, e.g a rest, which makes it a bit annoying to use (because actual start time is somewhere within that rest).

e = ()
Pgate(Pseq([Rest(0.1), d]), 1, \go_d).play(protoEvent: e)
(Pfunc{|ev| if(ev[\degree] == 4) {e[\go_d] = true}; ev} <> c).play

Since a Stream is a Routine, you could in theory can “hang it”, but this doesn’t quite work in combo with the EventStreamPlayer

q = Condition.new

// hang doesn't know how to "playAndDelta"; ibid if you use q.wait
(Prout({|ev| q.hang; loop { ev = ev.yield } }) <> d).play // err

This might be fixable, I don’t know for sure, but probably not because you cannot hang a nested routine that’s just having values pulled from with next (which is what EventStreamPlayer does to a stream).

q = Condition.new;
fork { 0.5.wait; "started ...".postln; {q.hang}.r.next;  "... and finished.".postln };

// to make that work, the outer routine would have to yield the value received
fork { 0.5.wait; "started ...".postln; {q.hang}.r.next.yield;  "... and finished.".postln };
q.unhang;

Actually that wasn’t too hard to make work:

EventStreamPlayerH : EventStreamPlayer {

	prNext { arg inTime;
		var nextTime;
		var outEvent = stream.next(event.copy);
		case
		{outEvent.isNil} {
			streamHasEnded = stream.notNil;
			cleanup.clear;
			this.removedFromScheduler;
			^nil
		}
		{outEvent === \hang} { ^outEvent } // the only addition basically
		{
			nextTime = outEvent.playAndDelta(cleanup, muteCount > 0);
			if (nextTime.isNil) { this.removedFromScheduler; ^nil };
			nextBeat = inTime + nextTime;	// inval is current logical beat
			^nextTime
		};
	}
}

Test that with

d = Pbind(*[dur: 0.3, ctranspose: 12, degree: Pseries(0, 1, 8)])

q = Condition.new
p = (Prout({|ev| q.hang; loop { ev = ev.yield } }) <> d)

r = EventStreamPlayerH(p.asStream, ()).play
q.unhang

Actually, that change/hack into ESP not entirely necessessay as you can do instead

(
d = Pbind(*[dur: 0.3, degree: Pseries(0, 1, 8)])
q = Condition.new;
(Prout({|ev| (play: { q.hang }, dur: 0).yield; loop { ev = ev.yield } }) <> d).play;
)

q.unhang;

Actually, 2nd workaround, which avoids even generating an extra event, but does change the \finish of the first event; although you can even chain the old finish. I’m not doing that in the example below, for simplicity (you’d have to test if the old one is not nil before executing it).

(
d = Pbind(*[dur: 0.3, degree: Pseries(0, 1, 8)]);
q = Condition.new;
(Prout({|ev| ev[\finish] = { q.hang }; loop { ev = ev.yield } }) <> d).play;
)

q.unhang;

More sophisticated ideas were discussed in Time-aware merging of two Event Pattern streams.

2 Likes