Adding a node to an NRT Score

Hi -
I’m currently trying to run a NodeProxy in an NRT score. I’m assuming I need to add it to the NRT score at the outset - I was hoping something like this would work:

nrtScore.add([0.1, [\d_recv, nodeInst]]);

But this fails when the code is run.

Also, is there a convenient list of all of these types of commands (\d_recv, \d_load, etc…)…?

Thank you.

Since you’re trying to run things in NRT anyway, how about extracting the graph function from the NodeProxy and putting into a SynthDef instead? I don’t think JITLib has support for NRT operation, but I’m not sure…

There is indeed: link

1 Like

Can anyone confirm that JITLib will not work in NRT operation?
There would be some real benefits, if it did.

It’s possible, but not as transparent as you want.

The centerpiece of JITLib is NodeProxy.

So if you want to do anything unusual with JITLib, NodeProxy and its parent class BusPlug are the first places to look.

Now… the question at hand wouldn’t be obvious because you have to dig into internals that JITLib takes pains to hide.

NodeProxy’s main methodology is to collect server messages into bundles before sending. You can take a peek into this by doing NodeProxy.browse, then type bundle into the search box, choose “NodeProxy + subclass methods,” and GO. Then you will see a whole bunch of methods like loadToBundle, prepareToBundle, sendAllToBundle…

These are normally treated as private methods (hence, lack of documentation). But, reading the source code, and a bit of back-tracing, turns up:

n = NodeProxy(s, \audio, 2).source_({ SinOsc.ar.dup });
// ^^ or Ndef(...) or ~abc in a proxyspace

// to create the nodeproxy but not `play` to output
b = MixedBundle.new;
n.wakeUpToBundle(b);

// ^^ now `b` is ready to use, see below

// or, to `play` it to the output
b = MixedBundle.new;
n.playToBundle(b, 0, 2);

Now you have access to all of the messages that are needed to initialize the NodeProxy:

  • b.preparationMessages – sends the SynthDef, creates the proxy’s group
  • c = b.messages.collect(_.value); – s_new messages

And you can add these messages into the score, e.g.

x = Score.new;
b.preparationMessages.do { |msg| x.add([0, msg]) };
b.messages.do { |msg| x.add([0.0001, msg.value]) };

There, I’m assuming that you want to start playing at the beginning. You should decide your own times accordingly.

hjh

1 Like
n = NodeProxy(s, \audio, 2).source_({ (SinOsc.ar * 0.1).dup });

// "play-it" messages
b = MixedBundle.new;
n.playToBundle(b, 0, 2);

x = Score.new;
b.preparationMessages.do { |msg| x.add([0, msg]) };
b.messages.do { |msg| x.add([0.1, msg.value]) };

// "stop-it" messages
b = MixedBundle.new;
n.objects.do { |obj| obj.stopToBundle(b) };
n.monitor.stopToBundle(b);

b.preparationMessages.do { |msg| x.add([2.0, msg]) };  // empty this time
b.messages.do { |msg| x.add([2.0, msg.value]) };


s.boot;

x.play;  // or x.recordNRT etc

hjh

1 Like

Hi James -
Thank you for the example - it got me a lot further along than I expected to go with this idea.
I’m still trying to understand this a little better, though, since I’m not used to this particular workflow.

What I’d like to do is use the Node “tree” structure, but have it branch at different times.

I’ll include the code below and annotate with my understanding - and maybe you can let me know where I’m missing the important details.

//establish a score, a bundle, and a node.

x = Score.new;
b = MixedBundle.new;
n = NodeProxy.audio(s, 2);
//add a few things for the node to do. 
n[0] = {Impulse.ar(490)};
n[1] = \filter->{|sig| SVF.ar(sig*0.2, LFDNoise0.ar(3).range(100, 9000), 0.8);};


// "play-it" messages
n.playToBundle(b, 0, 2);   //arguments: bundle, output channel, numChannels) puts current node into bundle.

//the following two load the bundle messages into the score. 
b.preparationMessages.do { |msg| x.add([0.1, msg]) };
//b.messages.do { |msg| x.add([0.1, msg.value]) };


//adding a new thing for a node to do...
n[2] = \filter->{|sig| sig*SinOsc.ar(8120);};
//n.playToBundle(b, 0, 2);   //the node is already added, so this seems redundant
b.preparationMessages.do { |msg| x.add([1, msg]) };
b.messages.do { |msg| x.add([1, msg.value]) };

//does not seem to play initial part..
x.play; 

Hi there -
I still haven’t managed to figure out this side of the code yet. Could anyone help explain what “preparationMessages” are - and maybe if there is a way to expand a node tree in NRT using them?

At this point I think you will need input from JITLib authors such as @julian or @adc . I had made some suggestions to get you started, but I am only an occasional user of JITLib. I’m not deeply enough intimate with JITLib internals to anticipate every possible scenario, and I’m afraid my summer holiday is over, I’m much busier than I was 3 weeks ago.

You’re doing something that, to my recollection, nobody has attempted in my 20 years of involvement in SC. There isn’t an established procedure for JITLib in NRT. That means it’s likely to take some research on your part to get there: open up the JITLib classes and get your answers from the code. Admittedly this isn’t easy :slightly_frowning_face: but it may be better in the long run to have your own independent knowledge. The idea is to figure out what the JITLib methods are doing, and emulate those processes to extract the server messages to be inserted into the Score.

The centerpiece is NodeProxy’s put method.

I apologize that my earlier post wasn’t enough… I’m afraid there isn’t much more that I can personally do at this point, though.

hjh

Hi @polina.v – it would be very nice to have a solution for this. Note that it will be limited in so far as a NodeProxy can contain also Patterns etc. so it may be necessary for the time being to exclude this possibility.

It is tricky, but can be made work. As you also found, a possible central piece is MixedBundle, which – sorry – misses documentation, but preparationMessages are documentaed in the superclass OSCBundle.

Optimally, we would have a way to capture all that is necessary for a score from the open bundle of the server, like:

s.openBundle;
n = NodeProxy.audio(s, 2);
//add a few things for the node to do. 
n[0] = {Impulse.ar(490)};
n[1] = \filter->{|sig| SVF.ar(sig*0.2, LFDNoise0.ar(3).range(100, 9000), 0.8);};
s.addr.bundle.postcs;
s.closeBundle(false); // close and ignore

You can see that there are syncFlag items in the bundle. The BundleNetAddr in the server uses them to do the sync handshake (when resources like SynthDefs are sent). Now if one could convert the content of a BundleNetAddr into a score, that would be generally useful – a lot more could be done this way.

I am not sure if I have overlooked anything.

Hi @julian -
Thanks for chiming in on this.
I think I’m still missing how this all ties together in a score, though.

My understanding is that the code that you’ve shared creates a bundle with NodeProxy indices included. I’m assuming what needs to happen next involves establishing a Score and a MixedBundle, where the current bundle can be pared with score messages, as in @jamshark70 's example.

Something like the following seems like it would be “close” - but I’m obviously still missing something about this format:

s.openBundle;
n = NodeProxy.audio(s, 2);
//add a few things for the node to do. 
n[0] = {Impulse.ar(490)};
n[1] = \filter->{|sig| SVF.ar(sig*0.2, LFDNoise0.ar(3).range(100, 9000), 0.8);};
s.addr.bundle.postcs;
s.closeBundle(false); // close and ignore

b = MixedBundle.new;
x = Score.new;
n.playToBundle(b, 0, 2);
b.preparationMessages.do { |msg| x.add([0, msg]) };
b.messages.do { |msg| x.add([0.1, msg.value]) };
x.play;

The thing that is missing here is: after closing the bundle, you want access to the bundle’s contents!

Julian’s suggestion throws away the bundle lets you access the bundle before closing it. But then closing the bundle throws it away, so there was no way to add it to the Score. This led you to believe that you needed to re-collect the bundle.

Also, collecting the bundle is slightly complicated by the fact that JITLib’s MixedBundles are sent asynchronously. If you run his entire code block at once, closeBundle happens before the bundles are sent, and the bundle ends up being empty.

Also, we should guard against errors.

So:

(
~getNodeProxyBundle = { |jitFunc, action|
	var result;
	if(s.addr.isKindOf(BundleNetAddr)) {
		Error("Cannot bundle while another bundle is in progress").throw;
	};
	fork {
		protect {
			s.openBundle;
			jitFunc.value;
			0.01.wait;  // arbitrary, just a guess; s.sync didn't help
		} { |error|
			if(error.isNil) {
				result = s.closeBundle(false);  // close, don't send, but keep the bundle
				action.value(result);
			} {
				s.closeBundle(false);  // on error, bundle is not reliable, so discard
			}
		};
	};
};
)

Then, when you inspect the bundle, you’ll find that it has two types of array elements:

  • [ \syncFlag, [ [ … msg …], [ … msg… ] ] ]
  • or just a flat message array

Sync doesn’t matter in NRT, so the code to add to the Score will need to flatten this.

(
x = Score.new;

~getNodeProxyBundle.({
	n = NodeProxy.audio(s, 2);
	n[0] = {Impulse.ar(490)};
	n[1] = \filter->{|sig| SVF.ar(sig*0.2, LFDNoise0.ar(3).range(100, 9000), 0.8);};
}, { |bundle|
	bundle.do { |row|
		if(row[0] == \syncFlag) {
			row[1].do { |msg|
				x.add([0, msg]);
			}
		} {
			x.add(0, row);
		}
	};
});
)

(In this example, I’m just adding all the messages at the beginning, time = 0 – I’ve run out of time for now; you will have to figure out how to handle the timing according to your requirements.)

hjh

Thanks for continuing – yes, my bad, I checked in that closeBundle later, after I had tested the code.

Btw. the whole thing has nothing to do with MixedBundle or JITLib, the syncFlag is added whenever there is a sync message somewhere in some code. So it is generally useful.

Once this works, we should make it a method for server.

Sorry to be obtuse here, but I’m still a little in the dark…

Here’s what I understand:
The ~getNodeProxyBundle function takes two arguments - the jitFunction (the nodes) and a set of timing messages about when to instantiate the node functions. To check the timing messages, adding something like “row.postln;” in the bundle.do function seems to spit out what you were mentioning. I am unclear about what these two different array types signify, though - though the one beginning with \syncFlag looks similar to what would go in an OSC score (Int8 arrays) - I don’t know what the other flat message array is for, entirely.

Is the idea of “flattening” to exclusively collect the Int8 Arrays within the Bundle (around “action.value(result)” ?)

Thanks for the continued help. I’m sure this will be useful to other SC users in the future also.

The code example shows you…

In these arrays, if it looks like a message, then it’s a message, and it should be added to the score.

The difference is how to locate the message arrays.

  • If the row is just an array, then it’s a message and just add it as is.

  • If the row begins with syncFlag, then the second array element is an array of messages.

But you can read this for yourself, just read the number of brackets carefully.

BTW Int8Array is totally irrelevant to your question. It is only the binary SynthDef, nothing more. It’s an argument to the d_recv message.

hjh

Ah, I see - that’s the conditional statement in the bundle function.

The issue is that it throws an ‘at’ error when I try to play the score, so I misread your comment and thought you were saying I needed to modify the code in order to flatten.

ERROR: Message 'at' not understood.
Perhaps you misspelled 'as', or meant to call 'at' on another receiver?
RECEIVER:
   Float 0.020000   47AE147B 3F947AE1
ARGS:
   Integer 0
CALL STACK:
	DoesNotUnderstandError:reportError
		arg this = <instance of DoesNotUnderstandError>
	Nil:handleError
		arg this = nil
		arg error = <instance of DoesNotUnderstandError>
	Thread:handleError
		arg this = <instance of Thread>
		arg error = <instance of DoesNotUnderstandError>
	Thread:handleError
		arg this = <instance of Routine>
		arg error = <instance of DoesNotUnderstandError>
	Object:throw
		arg this = <instance of DoesNotUnderstandError>
	Object:doesNotUnderstand
		arg this = 0.02
		arg selector = 'at'
		arg args = [*1]
	< FunctionDef in Method Score:play >
		arg i = 3
		var deltatime = nil
		var msg = nil
	Integer:do
		arg this = 6
		arg function = <instance of Function>
		var i = 3
	< FunctionDef in Method Score:play >  (no arguments or variables)
	Routine:prStart
		arg this = <instance of Routine>
		arg inval = 16.173789982
^^ ERROR: Message 'at' not understood.
Perhaps you misspelled 'as', or meant to call 'at' on another receiver?
RECEIVER: 0.02

Ok, I think I see it – I’m an infrequent user of Score, and after working out all those other details, I made a mistake on one of the add calls – in the else branch, I wrote x.add(time, msg) instead of the correct x.add([time, msg or msgs]).

But here I wonder – you’re actively working with Score, so its interface should be more fresh in your mind. So you could have thought to yourself, “huh, that add usage looks a bit funny, maybe he made a mistake and I could fix it” … which would have gotten you to the answer faster.

From my perspective: I’ve already spent close to an hour and a half on this thread. (It does actually take that long…) At the time of posting that last code example, I was 40-45 minutes into it and I had already run out of time. So I missed a detail. It won’t be the last time that happens. But then… If it’s always up to me to get every last detail 100% correct all the time, then the forum becomes as much pressure as a job, but without pay. Somehow this seems not quite ideal. So I’d encourage you and others to read code more actively and don’t be afraid to find and fix mistakes (don’t assume that someone else has done everything for you).

hjh