Ndef simple channel expansion question

In difference to that, SinOsc doesn’t call asAudioRateInput on any of its inputs

CombN : PureUGen {

	*ar { arg in = 0.0, maxdelaytime = 0.2, delaytime = 0.2, decaytime = 1.0, mul = 1.0, add = 0.0;
		^this.multiNew('audio', in.asAudioRateInput(this), maxdelaytime, delaytime, decaytime).madd(mul, add)
	}
}

SinOsc : PureUGen {
	*ar {
		arg freq=440.0, phase=0.0, mul=1.0, add=0.0;
		^this.multiNew('audio', freq, phase).madd(mul, add)
	}

Also

Ndef.findRespondingMethodFor(\asAudioRateInput)
// -> AbstractFunction:asAudioRateInput

and that does

	asAudioRateInput { |for|
		var result = this.value(for);
		^if(result.rate != \audio) { K2A.ar(result) } { result }
	}

So it calls something like Ndef-instance.value(CombN-instance) which seem to try to guess the rate (in BusPlug.value)

	value { | something |
		var n;
		if(UGen.buildSynthDef.isNil) { ^this }; // only return when in ugen graph.
		something !? { n = something.numChannels };
		^if(something.respondsTo(\rate) and: { something.rate == 'audio'} or: { this.rate == \audio }) {
			this.ar(n)
		} {
			this.kr(n)
		}

	}

But the big catch is that numChannels is always 1 for UGens, as far as I understand it! The help says

Discussion:
For a UGen, this will always be 1, but Array also implements this method, so > multichannel expansion is supported. See Multichannel Expansion.

So since something is a CombN instance in our case something.numChannels is going to be 1 there.

It was actually more mysterious to me how it works (properly) when asAudioRateInput isn’t in the call chain, i.e. what it usually does e.g. in the case of SinOsc etc. to guess the number of channels properly.

Actually, that does

	*multiNewList { arg args;
		var size = 0, newArgs, results;
		args = args.asUGenInput(this);

and then

	asUGenInput {
		^this.value(nil)
	}

So funnily enough it goes also in the same BusPlug.value function but with ignored (nil) argument instead of a Ugen for something! And that calls Ndef.ar(nil) instead of 1 as arg. Apparently there’s magic in Ndef.ar (actually BusPlug.ar) that returns all the channels on nil arg. Yeah, it’s on the last line of that method:

	ar { | numChannels, offset = 0, clip = \wrap |
		var output;
		if(this.isNeutral) {
			this.defineBus(\audio, numChannels)
		};
		this.prepareOutput;
		output = InBus.ar(bus, numChannels, offset, clip);
		// always return an array if no channel size is specified
		^if(numChannels.isNil) { output.asArray } { output }
	}

By the way, that seems to rely on an undocumented feature in InBus that seems to return all channels if nil is provided as 2nd arg!

b = Bus.audio(s, 3)
InBus.ar(b)
// -> [ an OutputProxy, an OutputProxy, an OutputProxy ]

InBus.ar(b, 2)
// -> [ an OutputProxy, an OutputProxy ]

// An here's another obscure feature:
InBus.ar(b, 4)
// -> [ a XFade2, a XFade2, a XFade2, a XFade2 ]

Yeah, the code is self-documenting:

InBus { // ...
	*new1 { |rate, bus, numChannels, offset, clip|
		var out, index, n, busRate;
		bus = bus.value.asBus;
		busRate = bus.rate;
		n = bus.numChannels;
		numChannels = numChannels ? n;

So it seems to me the fix is to have BusPlug.value check if the class of something is a Ugen, and in that case just make n = nil before calling .ar because that magically gets the whole bus thanks to InBus smarts.