NodeProxy reshaping behavior

Hi all, a technical question on NodeProxy/Ndef.

As I understand from reading the NodeProxy help file, if a proxy’s reshaping attribute is nil, and a source with a channel size > proxy channel size is provided, the channels will “wrap” and be mixed to fit the proxy’s size. For example, if a 4-channel signal is provided for a 2-channel proxy’s source, then channels 1 and 3 will be mixed in the left, while 2 and 4 will be mixed on the right — similar to binary operations with differently-sized arrays:

[ 10, 20 ] + [ 5, 6, 7, 8 ]; // --> [ 15, 26, 17, 28 ]

However, this seems not to be the case. In the following example, I can clearly hear that channels 1 and 2 are in the left ear, while 3 and 4 are in the right ear. This is not what I would consider “wrapping.” Am I misunderstanding something?

s.boot;

Ndef(\sines).play;

// a 2-channel source
Ndef(\sines, { SinOsc.ar([425,500]) * Decay2.ar(Impulse.ar([2,3]), 0.005, 0.3, 0.1) });

// substitute a 4-channel source
Ndef(\sines, { SinOsc.ar([425,500,750,850]) * Decay2.ar(Impulse.ar([2,3,4,5]), 0.005, 0.3, 0.1) });

Eli

I’m not sure why the help file says it wraps. If you look at the source for NumChannels where this behavior is implemented, it is just clumping the channels and then mixing them down:

NumChannels {

	*ar { arg input, numChannels = 2, mixdown = true;

		if(input.size > 1) { // collection
			^input
				.clump(roundUp(input.size / numChannels))
				.collect { arg chan, i;
					if(chan.size == 1) {
						chan.at(0)
					} {
						if(mixdown) {
							Mix.new(chan)
						} {
							chan.at(0)
						}
					}
				}
		} {
			// single ugen or single item collection
			if(input.isSequenceableCollection) {
				input = input.at(0);
			};

			if(numChannels == 1) {
				^input
			} {
				^Array.fill(numChannels, input)
			}
		}
	}
}

The result from your test is exactly what you would expect from this. It’s easy to see if you imagine the input as an array of scalar numbers:

(
~numChans = 2;
~input = [0, 1, 2, 3];
~clumps = ~input.clump(~input.size / ~numChans); // -> [ [ 0, 1 ], [ 2, 3 ] ]
~mixdown = ~clumps.collect {|chan| Mix.new(chan)}; // -> [ 1, 5 ]
)

Wrapping behavior would look like this:

( // wrapping behavior
~numChans = 2;
~input = [0, 1, 2, 3];
~clumps = ~input.clump(~numChans).flop; // -> [ [ 0, 2 ], [ 1, 3 ] ]
~mixdown = ~clumps.collect {|chan| Mix.new(chan)}; // -> [ 2, 4 ]
)

So if you want wrapping behavior in NodeProxies, you could overwrite NumChannels.ar accordingly:

+ NumChannels { // SystemOverwrite
    // Apply wrapping behavior to NodeProxies with static reshaping
	*ar { arg input, numChannels = 2, mixdown = true;

		if(input.size > 1) { // collection
			^input
				.clump(numChannels)
                .flop
				.collect { arg chan, i;
					if(chan.size == 1) {
						chan.at(0)
					} {
						if(mixdown) {
							Mix.new(chan)
						} {
							chan.at(0)
						}
					}
				}
		} {
			// single ugen or single item collection
			if(input.isSequenceableCollection) {
				input = input.at(0);
			};

			if(numChannels == 1) {
				^input
			} {
				^Array.fill(numChannels, input)
			}
		}
	}
}
1 Like

Thanks for this. My issue is not that I need NodeProxy to wrap its channels — my issue is that the help files say one thing but SC does another. I’m toward the tail end of a book-writing project, and it’s causing me to double-check (read: second-guess) everything I’ve ever known about SC. I don’t want to say “the channels wrap” just because the help files say so.

It seems like the documentation language should be changed to reflect the actual behavior…yes?

I agree, it’s probably better to fix the documentation than to change the implementation. Unfortunately, this is not the only place where the documentation is incorrect. I have lately found myself spending more time looking at the source code, rather than relying on the help. It can be pretty daunting, but you can be much more certain about how things work that way, and it’s an amazing learning experience!

1 Like