RunningSum bug: positive input --> small negative output

I’ve just encountered a strange case. I thought it might be better to discuss it here before logging a bug.

I haven’t been able to reduce it down any further than this. The problem occurs with:

  • Multiple SinOscs;
  • Through independent shapers with a specific transfer function;
  • Where two out of every four channels are delayed by a small amount;
  • And tanh distorted;
  • Then one of the non-delayed channels is RMS’ed.

The RMS sometimes reports as a small negative number. This should be impossible as all of the input values are real, and there is no real number whose square is < 0 – so the sum of the squares should be guaranteed positive. But it isn’t.

-> localhost
distAmp: -0.00290428
-> localhost
distAmp: -0.00285521

Unfortunately, if I remove any of the above elements, then the issue is no longer reproduced. 1 or 2 SinOscs, no problem; 3 or 4, problem. I couldn’t reproduce the issue with a simple linear transfer function. I couldn’t reproduce the issue with fewer than two delays (if numOscs = 2, there’s one DelayC, but I didn’t see the problem in that case – even though none of the delayed channels is going into the running sum!).

It’s moderately sporadic. With numOscs = 3, it posts a negative number about half the time, or more. With numOscs = 4, it occurs almost every time.

RunningSum’s source does something fancy to try to counteract floating-point rounding error. I’m guessing that I accidentally discovered a signal where the algorithm doesn’t work. But I just don’t see it.

hjh

(
s.waitForBoot {
	var out = Array.new,
	theta = 0.0,
	n = 512,
	baseRate = 2pi / n,
	endRate = 12;

	SynthDef(\distBass, { |out, freq = 440, gate = 1, t_gate = 1, freqlag = 0.1, freqRand = 1.008, amp = 0.1,
		preamp = 0.1, bufnum = 0, preTanhAmp = 20,
		atk = 0.01, dcy = 0.1, sus = 0.6, rel = 0.1,
		delay = 0.012, distMix = 0.5, hpfreq = 260|

		// HERE: numOscs < 3 does not reproduce the issue
		var numOscs = 3;

		var lowFRand = freqRand.reciprocal,
		fRand = Array.fill(numOscs, { 1 /* TExpRand.kr(lowFRand, freqRand, t_gate) */ }),
		sig = SinOsc.ar(Lag.kr(freq * fRand, freqlag)),
		eg = EnvGen.kr(Env.adsr(atk, dcy, sus, rel), gate, doneAction: 2),
		distAmp,
		distorted = Shaper.ar(bufnum, sig * preamp);
		// decorrelate
		distorted.do { |chan, i|
			if(#[1, 2].includes(i % 4)) {
				distorted[i] = DelayC.ar(chan, 0.1, SinOsc.kr(Rand(0.1, 0.2), Rand(0, pi), delay * 0.5, delay));
			};
		};
		distorted = Mix((distorted * (preamp * preTanhAmp)).tanh.clump(2));
		distAmp = RunningSum.ar(distorted[0].squared, 441).sqrt;
		Poll.ar(Trig1.ar(distAmp < 0, 1), distAmp, "distAmp");
	}).add;

	if(~shapebuf.isNil) {
		while { theta < 6pi } {
			out = out.add(sin(theta));
			theta = theta + (baseRate * theta.linexp(0, 6pi, 1, endRate));
		};

		out = (out.drop(1).reverse).neg ++ out;
		out = out.resamp1(2048).as(Signal);
		~shapebuf = Buffer.sendCollection(s, out.asWavetable);
	};

	s.sync;
	(instrument: \distBass, bufnum: ~shapebuf, midinote: 24,
		ffreq: 261.4, fDecay: 0.17, fegAmt: 5, preamp: 0.26, rqDecay: 1,
		out: 1000, sustain: 1
	).play;
};
)