Stereo issue with my diy auto harmonizer effect

Here’s a voice harmonization SynthDef I’ve been working on. It’s based on the medieval practice of Fauxbourdon.
The idea is that the pitch shift synth recognizes the scale degree of the input signal using Tartini, then uses that information to pick the right interval for the lower voice (called voice2 here) based on the scale degree. voice1 is just parallel fourths with the input signal and doesn’t require pitch recognition.

The tricky bit is the spatialization at the end: As I’ve posted it here, the output is noticeably louder in the left headphone than in the right.

If I add a !2 to the parameters of Out, like so …

Out.ar(\out.kr(0), sig!2 * \amp.kr(0.8));

I get a balanced stereo output.

However, I would like to distribute the voices over the stereo field so that each voice has its own position.

What might cause this behavior? How would I go about solving this issue without just duplicating the signal?

(
~rootnum = 62; // D4

~makeBuffers = {
	~diff = Dictionary.new;
	~diff.add(\maj -> [ 5/8, 3/5, 3/5, 5/8, 3/5, 5/8, 3/5, 5/8, 3/5, 3/5, 5/8, 3/5 ]); 
	~diff.add(\min -> [ 3/5, 5/8, 3/5, 5/8, 3/5, 3/5, 5/8, 3/5, 5/8, 3/5, 5/8, 3/5 ]);

		~diffbuf = Buffer(s, ~diff[\min].size, 1);
		// use ~diff[maj] for major
		s.listSendMsg(~diffbuf.allocMsg(~diffbuf.setnMsg(0, ~diff[\min])).postln);
};
~makebuffers.value();

SynthDef(\pitchshift, {
	var freq, hasFreq, sig, voice1, voice2, ratio1, ratio2, root;

	root = \rootmidinum.kr(~rootnum) %12;

	ratio1 = \r1.kr(0.75);

	sig = In.ar(\in.ir(0), 1); // expects mono input

	# freq, hasFreq = Tartini.kr(sig, 0.93, \nsize.kr(2048), \ksize.kr(0), \overlap.kr(1024), \smallCutoff.kr(0.5));

	f = freq.cpsmidi.round(1) + root;

	ratio2 = WrapIndex.kr(~diffbuf.bufnum, f);

	voice1 = PitchShiftPA.ar(
		in: sig,
		freq: freq,
		pitchRatio: ratio1,
		formantRatio: 1,
		minFreq: 20,
		maxFormantRatio: 10,
		grainsPeriod: 2,
		timeDispersion: 5
	) * \v1amp.kr(0.dbamp);
	voice2 = PitchShiftPA.ar(
		in: sig,
		freq: freq,
		pitchRatio: ratio2,
		formantRatio: 1,
		minFreq: 20,
		maxFormantRatio: 10,
		grainsPeriod: 2,
		timeDispersion: 5
	) * \v2amp.kr(0.dbamp);
	sig = [voice2, sig, voice1];
	sig = Splay.ar(sig);

	Out.ar(\out.kr(0), sig * \amp.kr(0.8));
	Out.ar(\fx.kr(0), sig * \send.kr(0.25));
}).add;

~pitch = Synth(\pitchshift, [\rootmidinum, ~rootnum, \in, SoundIn.ar(0), \out, 0]);
)

(This is an abbreviated version of the code I’m actually working with, a repository with the full project is here.)

This problem is often the result of UGens being double-wrapped in arrays.

To check, I added some debugging posts:

	sig = [voice2, sig, voice1].debug("sig before");
	sig = Splay.ar(sig);
	sig.debug("sig after Splay");

sig before: [ [ a BinaryOpUGen ], an OutputProxy, [ a BinaryOpUGen ] ]
sig after Splay: [ [ a BinaryOpUGen, a BinaryOpUGen ] ]

… whereas what you want is:

sig before: [ a BinaryOpUGen, an OutputProxy, a BinaryOpUGen ]
sig after Splay: [ a BinaryOpUGen, a BinaryOpUGen ]

PitchShiftPA is collect-ing over the channels, but not removing the array in the case of one channel. Splaying these ends up with an extra array level. This causes the two channels to be multichannel-expanded into two Out units, both pointing to the left channel.

Probably PitchShiftPA *ar should end with ^out.unbubble.

hjh

1 Like

is there a particular need to create a three-part array (trying to expand to three channels)? if you are not doing a particular spatialization, or for a start, I would personally start with

sig = voice2 + sig + voice1;

But good one with .debug, @jamshark70 - I was not aware of this nifty method.

1 Like

Splay mixes the array down to stereo, with each channel equally spaced across the stereo field.

hjh

That’s exactly what I’m going for – spatial separation of the voices. Otherwise the sig!2 method would work just fine.

Your suggestion led me onto the right path: I tried
sig = [voice2, sig, voice1].flat;
and it did the trick!

.unbubble worked, too.
I wasn’t aware of .debug – very useful!

Thank you!