Crossfading on a control bus jumps rather than fading

Why does this jump from -1 to 0, instead of fading?

I don’t expect the crossfade to take 5 seconds because writing back to the bus will alter the “old” signal – but I do expect it to take longer than one control block.

(
s.waitForBoot {
	k = Bus.control(s, 1).set(-1);
	b = Buffer.alloc(s, s.sampleRate * 2 / s.options.blockSize, 1);
	s.sync;
	r = {
		RecordBuf.kr(In.kr(k, 1), b, run: 1, loop: 0, doneAction: 2);
		Silent.ar(1)
	}.play;
	0.5.wait;
	a = {
		var old = In.kr(k, 1);
		var new = SinOsc.kr(0.1);
		var fade = Line.kr(-1, 1, 5);
		var t = Impulse.kr(0);
		var result = LinXFade2.kr(old, new, fade);
		[old, new, fade, result].poll(t);
		result
	}.play(outbus: k);
	1.5.wait;
	a.free;
	{ b.plot(minval: -1, maxval: 1) }.defer;
};
)

(“Dev” topic b/c I think this is a bug.)

EDIT: Here’s a more revealing case – old signal = 0.25, new signal begins at -1, the result jumps to 0 even though neither input signal is 0 and the xfade is not in the middle.

That points to an initialization bug – a 0 is being returned somewhere and written back onto the bus, corrupting the rest of the crossfade.

(
s.waitForBoot {
	k = Bus.control(s, 1).set(0.25);
	b = Buffer.alloc(s, s.sampleRate * 2 / s.options.blockSize, 1);
	s.sync;
	r = {
		RecordBuf.kr(In.kr(k, 1), b, run: 1, loop: 0, doneAction: 2);
		Silent.ar(1)
	}.play;
	0.5.wait;
	a = {
		var old = In.kr(k, 1);
		var new = SinOsc.kr(0.1, -0.5pi);  // negative cosine
		var fade = Line.kr(-1, 1, 5);
		var t = Impulse.kr(0);
		var result = LinXFade2.kr(old, new, fade);
		[old, new, fade, result].poll(t);
		result
	}.play(outbus: k);
	1.5.wait;
	a.free;
	{ b.plot(minval: -1, maxval: 1) }.defer;
};
)

hjh

If you just plot all the signals, it is fine:

(

    s.waitForBoot {
	k = Bus.control(s, 1).set(-1);
	b = Buffer.alloc(s, s.sampleRate * 2 / s.options.blockSize, 1);
	s.sync;
	r = {
		RecordBuf.kr(In.kr(k, 1), b, run: 1, loop: 0, doneAction: 2);
		Silent.ar(1)
	}.play;
	0.5.wait;
	a = {
		var old = In.kr(k, 1);
		var new = SinOsc.kr(0.1);
		var fade = Line.kr(-1, 1, 5);
		var t = Impulse.kr(0);
		var result = LinXFade2.kr(old, new, fade);
        Out.kr(k, result);
		[old, new, fade, result]
	}.plot(5)
};
)

so maybe it has to do with .play on a control signal?

Ohhhhhh right… .play adds an envelope with a short fade.

OK, so, not really a UGen bug. So then what’s happening is that the synth initially outputs 0 (because of the fade envelope), and this is written onto the bus, and (because control buses don’t reset their values with each control cycle), this 0 then becomes the “old” signal for the crossfade.

However we might want to consider removing the envelope for control-rate {}.play.

(
a = { |freq = 75, ffreq = 300, rq = 0.2|
	BLowPass4.ar(
		Saw.ar(freq * [1, 1.005]),
		ffreq, rq
	) * 0.1
}.play;
)

k = Bus.control(s, 1);
k.set(300);

a.set(\ffreq, k.asMap);

// here's the oopsy
m = { LFTri.kr(0.1, -1).exprange(300, 1000) }.play(outbus: k);

m.free; a.free;
k.free;

The “oopsy” is: the kr bus is initialized to 300 Hz, and the modulator also starts at 300 Hz, so you’d expect the filter frequency to begin modulating seamlessly. Instead, you get a little “whoop” as the filter frequency drops to 0 momentarily and envelope-ramps back up. (I never noticed this before because I do control automation via my GenericGlobalControl class, where .automate doesn’t stick an envelope in.)

Nonzero fadeTime is OK for audio rate but I would argue that it’s generally not desirable for control rate (but it’s on by default, always). {}.play at control rate does not know the control value’s acceptable range and it is not safe for it to assume the user wants to begin and end at 0. I’m actually surprised that the 0 Hz cutoff didn’t blow the filter.

(Btw my sudden interest in this is that I had a video recording get wrecked by a sudden jump in a filter frequency when I started automation, so… ok, time to implement crossfading on automation in my system. While investigating whether or not I could do it on one bus, I found this issue. The final implementation keeps the buses separate, in any case: the old value/signal gets moved to a temp bus, and the new signal crossfades with that and writes onto the main bus. The temp bus and old synth go away when the crossfade is done!)

hjh