Replacing Impulse with TDuty is messing up a later calculation

I think this qualifies as a literal Heisenbug – there’s an incorrect value, but if I try to observe the wrong value, then it magically becomes the right value.

So, working flip-flop envelopes:

(
p = { |trigRate = 20, trigRandMul = 1,
	bufDur = 1.0, xfade = 0.008,
	endAmp = 0.7, grainEndAmp = 0.25, rate = 1, advance = 0,
	sustain = 1, beatsPerSec = 1|
	var susTime = sustain / beatsPerSec;
	var sr = SampleRate.ir;
	var trig = Impulse.ar(trigRate);
	var evenOdd = ToggleFF.ar(trig);
	var trigs = [evenOdd, 1 - evenOdd];
	var egs = EnvGen.ar(Env.adsr(xfade, trigRate.reciprocal.poll(Impulse.kr(0)), grainEndAmp, xfade), trigs);
	[trigs, egs].flat
}.plot(duration: 1)
)

OK, now I would like to randomize the IOI and know the duration of each random segment… use TDuty to calculate the IOI and pass it forward for the duration calculation.

(
p = { |trigRate = 20, trigRandMul = 1,
	bufDur = 1.0, xfade = 0.008,
	endAmp = 0.7, grainEndAmp = 0.25, rate = 1, advance = 0,
	sustain = 1, beatsPerSec = 1|
	var susTime = sustain / beatsPerSec;
	var sr = SampleRate.ir;
	var durStream = Ddup(2, (trigRandMul ** Dwhite(-1.0, 1.0, inf)) / trigRate);
	var trig = TDuty.ar(durStream, 0, durStream);
	var evenOdd = ToggleFF.ar(trig);
	var trigs = [evenOdd, 1 - evenOdd];
	var durs = Latch.ar(trig, trigs);
	var egs = trigs.collect { |t, i|
		EnvGen.ar(Env.adsr(xfade, durs[i], grainEndAmp, xfade), t)
	};
	[trigs, egs].flat
}.plot(duration: 1)
)

Now the envelope decay is much more gradual than it ought to be, meaning that durs[0] and durs[1] are too high.

How high? I tried: 1/ durs[i].poll(0) in the EnvGen; 2/ plotting [trigs, egs, durs].flat; 3/ SendReply.ar(Impulse.ar(1), '/durs', durs) printing in the language. In all cases, the envelope magically behaves correctly (exactly the same as the first graph), and the correct value is printed.

poll inserts a new ugen into the chain, so the input into EnvGen is not the same. But for 2 and 3, the same Latch output is split off into different places but the EnvGen receives the input directly. For 2, the RecordBuf must naturally follow EnvGen; for 3, .trace shows me that the SendReply comes after EnvGen as well.

I can’t think of a good reason why a later operation on a UGen’s output would affect an earlier reference to it, particularly when that later operation is not modifying the value but only sending it somewhere else.

(Actually re: the “question,” I also just realized that EnvGen inputs are read only at control rate, not audio rate, so I will have to find another way anyway. But this is really weird behavior; atypically, I don’t have any sort of explanation for this. So I’m kicking it out to the community. What the F—ourier is going on here?)

hjh

Have you checked dumpUGens? While trying to rework the ugen sorting, I found many issues like this - where adding later ugens would change the order of previous ones.

In this case, I’m not seeing that – all 3 cases keep UGens 0-13 the same (except that case 2 adds two channels to the Out at UGen index 13). Cases 1 and 3 add UGens but always after the main signal flow.

hjh

FWIW this is what I ended up with. It seemed like not a terribly complex requirement but it ended up requiring a fistful of workarounds. I compromised a bit on the shape of the decay, but, close enough.

(
p = { |trigRate = 30, trigRandMul = 2,
	bufDur = 1.0, xfade = 0.001,
	endAmp = 0.7, grainEndAmp = 0.25, rate = 1, advance = 0,
	sustain = 1, beatsPerSec = 1|
	var susTime = sustain / beatsPerSec;
	var sr = SampleRate.ir;
	var durStream = Ddup(2, (trigRandMul ** Dwhite(-1.0, 1.0, inf)) / trigRate);
	var trig = TDuty.ar(durStream, 0, durStream);
	var evenOdd = ToggleFF.ar(trig);
	var trigs = [evenOdd, 1 - evenOdd];
	var durs = Latch.ar(trig, trigs);
	// n-samples-th root of the decay target
	// b/c FOS below incrementally exponentiates this
	var coeffs = Demand.ar(trigs, 0, grainEndAmp) ** max(durs * sr, 100).reciprocal;
	var egs = trigs.collect { |t, i|
		var dirac = HPZ1.ar(t) > 0;
		FOS.ar(
			dirac,
			// DC.ar units to force b1 to be read at audio rate
			// to suppress feedback at the reset point
			a0: DC.ar(1), a1: DC.ar(0), b1: coeffs[i] * (dirac <= 0)
		) * EnvGen.ar(Env.asr(xfade, 1, xfade), t)
	};
	[trigs, egs].flat
}.plot(duration: 0.7)
)

hjh