Doubling of events when redefining Tdefs

Hi there, how can I avoid doubling of Events being played when a Tdef is redefined (assuming it’s running in a loop)?

For example, run this definition first (not playing; it will be forked by the second Tdef):

(
Tdef(\par, {
	3.do{
		(degree: 7, legato: 0.1, amp: 0.2).play;
		0.125.wait;
		(degree: 8, legato: 0.1, amp: 0.2).play;
		0.125.wait;
	}
});
)

Then run this (playing) Tdef:

(
Tdef(\main, {
	inf.do{
		Tdef(\par).fork;
		(degree: 0, legato: 0.2, amp: 0.7).play;
		0.25.wait;
		(degree: 2, legato: 0.2, amp: 0.5).play;
		0.75.wait;
	}
}).play(quant: 1);
)

If you go back and re-execute the \par or \main Tdefs (after modifying them or not), you will hear that their played Events get “doubled up” (sound louder), since two copies (or more, if you execute rapidly) are temporarily running – the old and new overlap for one iteration.

What would be the way to redefine a running Routine or Tdef and ensure this doesn’t happen (i.e., just use the new definition the next time through the loop)?

Thanks,
Glen.

OK… this is tricky.

TL;DR try play instead of fork. (TBH I could not figure out why you’re using fork here.)

There is a curious behavior that may or may not be related.

Tdef / TaskProxy inherit their stream-changing logic from PatternProxy.

With event patterns, each event goes through a process like this:

  1. The stream puts data into an Event object. At this point, no action is taken on the data; they are just collected into one place.
  2. The Event gets yield-ed back to the stream player.
  3. Then playAndDelta plays the event (the action is here), and waits for the next amount of time.

Changing a PatternProxy’s source in the middle should happen at the next quant time. The old source might have more events to play between “now” and “next quant.” So (see EventPatternProxy:constrainStream) the new stream is a Pseq, beginning with embedding the old pattern for exactly the number of beats needed to reach the next quant time.

What happens if that number of beats is 0?

  1. The stream puts data into an Event object.
  2. Psync realizes that the time has already expired, so it returns without playing that event = no action (this is important).
  3. Then the new stream takes over.

So with Pdef, I believe you wouldn’t see this problem:

(
Pdef(\x, Pbind(
	\counter, Pseries(0, 1, inf).trace(prefix: "num: "),
	\dur, 1,
	\type, \rest
)).quant_(1).play;
)

(
Pdef(\x, Pbind(
	\counter, (Pseries(0, 1, inf) * 5).trace(prefix: "num: "),
	\dur, 1,
	\type, \rest
));
)

Pdef(\x).stop;

Indeed, no doubled posting (but there’s something weird about that, too, which I don’t have time for at the moment).

Tdef inverts the order. The routine function must do the action before yielding the time delta. Then, same question, what happens if the time to the next quant is 0?

  1. The stream has already taken action. Now, it’s too late to cancel!
  2. Pconst realizes that the time has already expired, so it returns. (But it’s too late.)
  3. Then the new stream takes over.
Pconst(0.0, Pn(1, inf).trace(prefix: "dur: ")).asStream.nextN(3)

dur: 1  <<-- this is the thing you don't want
-> [ 0.0, nil, nil ]

Pconst could perhaps be changed to check first whether the total has been reached, before evaluating the stream. This is somewhat risky, though, as we have no way to know how much code in the wild relies on the current behavior. If someone, somewhere, is relying on Pconst to evaluate the stream at least once, even if the target total is 0, then changing it would break their code.

Or TaskProxy:constrainStream could be hacked to use a Prout instead of Pconst. (Edit: I tried this; it solves the problem when rerunning the Tdef(\main) block but not when rerunning Tdef(\par).)

Or a new Pconst could be added (but I’m not sure it’s a good idea to have two classes that do almost exactly the same thing).

Or you could refactor your code to yield events instead of play and wait. But I tried this and it didn’t solve the problem (which is probably the same issue: you’ve forked it once from the old stream before it figures out that it’s already time to switch to the new stream).

(
Pdef(\par, Prout {
	3.do {
		(degree: 7, legato: 0.1, amp: 0.2, dur: 0.125).yield;
		(degree: 8, legato: 0.1, amp: 0.2, dur: 0.125).yield;
	}
});
)

(
Pdef(\main, Prout {
	inf.do {
		Pdef(\par).fork;  // still doubled; change this to `play` and it's better
		(degree: 0, legato: 0.2, amp: 0.7, dur: 0.25).yield;
		(degree: 2, legato: 0.2, amp: 0.5, dur: 0.75).yield;
	}
}).play(quant: 1);
)

hjh

Thanks for the detailed investigation! I will need to take time to go through your response carefully.

But to answer this part:

Because that’s the way the Tdef help page says you “play an independent task in parallel”. Later on, in the code examples (“Embed and fork”), it also mentions:

// to start a tdef in its own separate thread, thus branching into parallel threads, 
// one can use .fork, or .playOnce

In my example here I am indeed generating Events (so, as you say, could switch to use Pdef instead of Tdef), but that was mainly just to keep the example simple. I wanted to try using Tasks instead of Patterns because I may be doing other (non-pattern) things, so I wanted to write it as “regular” code.

I suspect though that it’s exactly the “parallel” part that’s getting you into trouble.

From within one Tdef, you can .play another Tdef and they will be two separate streams running in parallel. The help may be misleading here. They are not merged like in Pspawner.

.fork allows multiple instances of a Tdef to run in parallel with themselves… which is great if you want that, but from your description, I’m not sure that’s what you’re after.

But there is some funky handling of the “redefine” feature. If “redefine” causes the next fork request to fork twice, that’s really weird. Maybe a bug. It’s hard to tell because I’m not sure anyone ever wrote down what is the correct behavior for this case.

With this, I can reproduce it, but not consistently:

(
Tdef(\count3, {
	3.do { |i|
		i.asInteger.debug("count3");
		0.125.wait;
	};
});

Tdef(\loop, {
	loop {
		Tdef(\count3).fork;
		"abcde".scramble.debug("loop");
		1.0.wait;
	}
});
)

Tdef(\loop).play;

Tdef(\count3).source = Tdef(\count3).source;

// sometimes:
loop: decba
count3: 0
count3: 0
count3: 1
count3: 1
count3: 2
count3: 2

Tdef(\loop).stop;

We may need @julian to figure out what’s wrong here. A single fork in the main loop should not double like this.

hjh

When you replace the definition while it is still playing, it will replace it everywhere. So in this case, e.g. when the forked task is at count 2, it will start at zero, which will then take longer, and a new one will also start in parallel.

You can see that it doesn’t happen as easily when you make the wait time longer:


(
Tdef(\count3, {
	3.do { |i|
		i.asInteger.debug("count3");
		0.125.wait;
	};
});

Tdef(\loop, {
	loop {
		Tdef(\count3).fork;
		"abcde".scramble.debug("loop");
		3.wait;
	}
});
)

Tdef(\count3).quant = nil;
Tdef(\loop).play;

Tdef(\count3).source = Tdef(\count3).source;

If you want to just fork the source, you can write Tdef(\count3).source.fork:


(
Tdef(\count3, {
	3.do { |i|
		i.asInteger.debug("count3");
		0.125.wait;
	};
});

Tdef(\loop, {
	loop {
		Tdef(\count3).source.fork;
		"abcde".scramble.debug("loop");
		1.wait;
	}
});
)

Tdef(\loop).play;

Tdef(\count3).source = Tdef(\count3).source;