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.