Ndef simple channel expansion question

In the Ndef graph below ‘s’ is stereo but ‘z’ is mono (Sc 3.11.2 & Sc 3.12.1):

Ndef('o', { LFSaw.kr(#[8, 7.23], 0) * 3 + 80 });
Ndef('f', { LFSaw.kr(0.4, 0) * 24 + Ndef('o') });
Ndef('s', { SinOsc.ar(Ndef('f').midicps, 0) * 0.04 });
Ndef('z', { CombN.ar(Ndef('s'), 0.2, 0.2, 4) * 0.1 });
Ndef('z').play

['o','f','s','z'].collect({ arg item; Ndef(item).bus.numChannels }) == [2, 2, 2, 1]

However if the CombN decaytime is set to [4, 4] (for instance) then ‘z’ is stereo as well.

All of ‘o’ ‘f’ ‘s’ and ‘z’ are ‘elastic’:

['o','f','s','z'].collect({ arg item; Ndef(item).reshaping }) == ['elastic', 'elastic', 'elastic', 'elastic']

Why doesn’t ‘s’ make ‘z’ expand? How should I write this instead?

Ps. Are either of the two expressions below the correct way to make ‘elastic’ the default rule?

Ndef(\nil).proxyspace.reshaping = 'elastic';
BusPlug.defaultReshaping = \elastic;

Sorry to pester about this, I’m sure I’m misunderstanding something very obvious!

I’ve checked it’s not some stray extension, it’s the same with only the standard library.

It seems that f is stereo because of o, and then s because of f, but then not z because of s.

Any help much appreciated.

Actually, I think I’ve stumbled upon the same question!

Randomly panned dust into a comb resonator, fine

Ndef(\comby, {CombL.ar(Pan2.ar(Dust.ar(7), pos: LFNoise0.kr(5)), delaytime: 0.015, decaytime: 4)}).play

Ndef(\comby).numChannels == 2

// But, written differently, the second Ndef does not expand to two channels, only get one:

Ndef.clear

Ndef(\dusty, {Pan2.ar(Dust.ar(7), pos: LFNoise0.kr(5))})

Ndef(\comby, {CombL.ar(Ndef(\dusty), delaytime: 0.015, decaytime: 4)}).play

Ndef(\comby).numChannels == 1

Even if I explicitly make the second Ndef two channels, still only hearing the left

Ndef.clear

Ndef(\dusty, {Pan2.ar(Dust.ar(7), pos: LFNoise0.kr(5))})

Ndef(\comby).mold(2,\audio)

Ndef(\comby, {CombL.ar(Ndef(\dusty), delaytime: 0.015, decaytime: 4)}).play

Ndef(\comby).numChannels == 2

I have tried elastic reshaping, but makes no difference.

Right, it looks to me like the problem is not with what I was trying to do, but with CombL. Other ugens expand as I would have expected:

Ndef(\dusty, {Pan2.ar(Dust.ar(7), pos: LFNoise0.kr(5))})
Ndef(\comby, {FreqShift.ar(Ndef(\dusty), freq: -500 )}).play
Ndef(\comby).numChannels // 2
Ndef(\comby).clear
Ndef(\comby, {Ringz.ar(Ndef(\dusty),freq: 440, mul: 0.2)}).play
Ndef(\comby).numChannels // 2
Ndef(\comby).clear
Ndef(\comby, {CombL.ar(Ndef(\dusty), delaytime: 0.015, decaytime: 4)}).play
Ndef(\comby).numChannels // 1

I always thought that the standard way to access a NodeProxy’s signal within another synthesis function is by calling .ar or .kr on it, depending on the rate:

Ndef(\ar2, { SinOsc.ar([440, 550]) });

LPF.ar(Ndef(\ar2).ar, 1000)  // two LPFs

LPF.ar(Ndef(\ar2), 1000)  // one LPF

I realize there’s an effort underway to eliminate the need for rate constructors/accessors, but such shouldn’t be assumed to be completely implemented everywhere at present. (Perhaps it’s a good idea to move in that direction…?)

hjh

1 Like

Thanks James, I hadn’t thought of that. Let me give that a try when I get home.

Ah, brilliant, thanks.

And if you don’t happen to know the rate of the proxy it does:

Ndef('z', { CombN.ar(Ndef('s').perform(Ndef('s').bus.rate.rateToSelector), 0.2, 0.2, 4) * 0.1 });

(I couldn’t find rateToSelector in the standard libraries, but it’s easy to write.)

Thanks kindly!

I looked at this some more.

‘f’ and ‘s’ both use math operators on the source Ndef:

  • ‘f’: Ndef(\o) is the second operand of a binary operator.
  • ‘s’: Ndef(\f) is the only operand of the unary operator midicps.

So these are handled by the AbstractOpPlug hierarchy – any math operator applied to any subclass of BusPlug will end up here.

UnaryOpPlug takes its number of channels from the source BusPlug. BinaryOpPlug determines its number of channels as the larger of the two operands (ignoring operands that haven’t been initialized yet – a = NodeProxy.new; b = NodeProxy.new; (a + b).numChannels is nil).

And, when a *OpPlug is resolved into something that can plug into a UGen (by asUGenInput.value), this requires resolving into the multichannel array.

So far so good.

‘z’ does not apply any math operators. So the Ndef is a direct input – that is, you’re not passing in the channels – you’re passing in some other object that represents the channels. By asUGenInput or asAudioRateInput, this other object is expected to convert itself into its channels.

JITLib reshaping logic appears to be that a source BusPlug should adapt itself to the number of channels that the target expects – this.value(for) where for is the signal processor for which the source is resolving itself.

UGens assume that they are single-channel. So Ndef(\s) is trying to resolve itself down to a single channel “for CombN.” (I confirmed using some debugging traces that AbstractFunction:asAudioRateInput is receiving for == CombN and subsequently, BusPlug:ar is being called with arg numChannels = 1 – so this is where it’s going askew.)

CombN.ar(Ndef(\s).ar) bypasses that logic – ar converts the proxy into the array of two channels, and then CombN must be stereo.

It’s still a bit mysterious – it looks like kr and ar inputs are treated differently: Ndef(\f).midicps is kr and resolves to two channels, but if I try CombN.ar(Ndef(\s) + 0) to force a math operation, CombN still squeezes this down to one channel.

So my conclusion for now is that to be safe, UGens require UGens (not proxies), or arrays of UGens, as inputs, and to get these from a NodeProxy, best practice is to call the rate method.

hjh

2 Likes

Thanks for the detailed analysis!

The Ndef rules are quite subtle, moreso than I’d understood.

However, setting the rate by reading from the bus parameter solves the immediate problem, thankyou!

Also, in case useful to anyone, the program below rewrites simple Ugen graphs as Ndef graphs.

I.e. the Ndef graph above is written:

var o = LFSaw.kr([8, 7.23], 0) * 3 + 80;
var f = LFSaw.kr(0.4, 0) * 24 + o;
var s = SinOsc.ar(f.midicps, 0) * 0.04;
CombN.ar(s, 0.2, 0.2, 4) * 0.1

It’s a (very!) simple Ast rewrite rule, but it does allow:

  • using the same notation for Ndef and UGen graphs
  • reevaluating binding expressions as the Ndef graph runs (obviously… thanks Ndef!)

https://gitlab.com/rd--/stsc3/-/blob/master/Language/Smalltalk/SuperCollider/Ndef.hs