Trigger a function when bus goes above a value or instead of doneAction

Hello,

I’m discovering SuperCollider and it looks awesome. Now I’d like to run a function after some specific events, but can’t find how to do. Two cases in particular are interesting to me:

  • many functions have a doneAction, like PlayBuf, but I can only input a number here. How can I run a function, for instance to start a new song when a buffer has finished to played?
  • when a bus/input goes above a threshold (e.g. pressing a button changes the bus from 0 to 1): I guess I could write an infinite loop that reads the input and compare it with the previous one but this seems quite cpu intensive and dirty.

Any advice would be welcome :slight_smile:

Couple of examples:

// threshold: fire the action when the LFSaw goes above 0.5

(
// if you have multiple actions, use a different name for each
// e.g. OSCdef(\xyzThreshold, ...)
OSCdef(\action, { |msg|
	// this is where you write your language-side action
	"got message, LFSaw value was %\n".postf(msg[3]);
}, '/threshold', s.addr);

a = {
	var sig = LFSaw.ar(0.2);  // 5 second cycle
	var threshold = (sig > 0.5);
	SendReply.ar(threshold, '/threshold', sig);
	Silent.ar(1)
}.play;

a.onFree { OSCdef(\action).free };
)

a.free;

The “done” case is similar but instead of a threshold, use the Done ugen to get a nonzero value when the source is done.

(
OSCdef(\action, { |msg|
	// this is where you write your language-side action
	"node % ended\n".postf(msg[1]);
}, '/envDone', s.addr);

a = {
	var sig = SinOsc.ar(440);
	var eg = EnvGen.kr(Env.perc(0.01, 1.0), doneAction: 2);
	var doneSignal = Done.kr(eg);
	SendReply.kr(doneSignal, '/envDone');
	(sig * eg * 0.1).dup
}.play;

a.onFree { OSCdef(\action).free };
)

hjh

1 Like

Great, thank you so much it does exactly what I wanted! Just a few questions:

  1. is there a SendReply-like function that does not involve OSC? Not only OSC seems like a waste of resource (need to create an OSC listener etc), but it seems also a bit dirtly, for instance it is not robust against name conflicts (if two libraries share the same \action name bugs will occur), it seems to involve some network communication (quite an overhead to run a simple function… which also open some security questions, for instance I would not be surprised to see cases where this allow attackers to run arbitrary code on the server etc), it creates a code a bit clutered (the function to run is far from the part that calls it, so I cannot use variables in the clojure without adding them in arguments etc) and I need to be extra careful about race conditions. I am maybe nitpicking here, put still curious.
  2. I also realized the existence of OSCFun().oneShot, and this seems to work as well and it is a bit easier to use (but maybe even less robust against race conditions if it is called in parallel, it may be better to create a random identifier for each call so that the free does not free all calls made in parallel). Am I missing nasty other edge-cases, where your above code should be prefered?
(
a = {
	var f = OSCFunc({"Done !".postln }, '/envDone').oneShot; // TODO: maybe create a different identifier when this can be run muliple times in parallel
	var sig = SinOsc.ar(440);
	var eg = EnvGen.kr(Env.perc(0.01, 1.0), doneAction: 2);
	var doneSignal = Done.kr(eg);
	SendReply.kr(doneSignal, '/envDone');
	(sig * eg * 0.1).dup
}.play;

)

No.

It’s important here to understand the division of labor between the language and the audio server.

The server only processes audio. The server cannot run language-side functions. It has no capacity to evaluate language-side code in any way. It only evaluates UGens in response to OSC messages sent to it. The only way to run language-side code in response to a server-side condition is for the server to send a message back to the language, where a listener is waiting for it.

You’re not wrong about some of the drawbacks, but I’d say the critique is rather exaggerated. It really isn’t that bad. On the language side, you’re defining your own OSC command paths – even if an attacker had your outward IP address, the probability of them guessing your exact command path is extremely low, and they could execute remote code in SC only if you receive a string over OSC and your code .interpret-s it – which is voluntary. Nowhere in the SC code base is this done.

Name conflicts are not hard to manage, and as you correctly found, a one shot OSCFunc avoids them entirely.

You can distinguish messages based on node ID and a second identifier, eliminating the problem that I think you’re calling “race condition.”

hjh

1 Like

I think NodeWatcher can be used in some cases:

(
SynthDef(\mySignal, { |synID = 0|
    var sig = SinOsc.ar;
    var eg = EnvGen.kr(Env.perc(0.01, 1.0), doneAction: 2);
    Out.ar(0, (sig * eg * 0.1).dup);
}).add;
)


x = Synth(\mySignal, [\synID, 100]);

NodeWatcher.register(x);

x.onFree {
    "Synth % freed!".format(x.nodeID).postln;
};

EDIT: Or is it implicit in your example, @jamshark70 ? Maybe it’s the same thing

And

That will handle the case of doneAction: 2 but NodeWatcher wouldn’t help with thresholds.

In any case, the point that was missed in the concern over a “race condition” is that SendReply includes two numbers apart from the command path and the values – the node ID from which the message is being sent, and an arbitrary “replyID” that is completely up to you. These are sufficient to distinguish the source of the message – OSCFunc/OSCdef’s built-in filtering features can make sure that the responder is firing only when receiving from the intended source.

a = Array.fill(3, { |i| Synth(\default, [freq: (i+3) * 100]) });

-> [ Synth('default' : 1000), Synth('default' : 1001), Synth('default' : 1002) ]

// so we've got 3 synths with different IDs
// now use argTemplate to filter based on node ID

// when a node ends, the server sends:
// ['/n_end', nodeID, a bunch of other stuff]
// `argTemplate: [synth.nodeID]` means that
// the OSCFunc will respond only if
// the first item after the command path matches

(
a.do { |synth, i|
	OSCFunc({ |msg|
		"Synth at index % with node ID % was freed\n"
		.postf(i, synth.nodeID)
	}, '/n_end', s.addr, argTemplate: [synth.nodeID]).oneShot;
};
)

// now release them all at once: release times should be the same
a.do(_.release);

Synth at index 1 with node ID 1001 was freed
Synth at index 0 with node ID 1000 was freed
Synth at index 2 with node ID 1002 was freed

The messages were not received in numeric order (but that’s okay because these are parallel synths, with no cause-effect dependency among them). But the link between the synth and its position in the array was preserved.

The same technique will work for the threshold case – here, the three synths have different thresholds, and you don’t see any wires getting crossed:

// similar for threshold triggers
(
a = Array.fill(3, { |i|
	{
		var sig = LFSaw.kr(Rand(0.5, 3));
		var thresh = (sig > (i * 0.5 - 0.5));
		SendReply.kr(thresh, '/thresh', sig);
		Silent.ar(1)
	}.play;
});
)

-> [ Synth('temp__0' : 1003), Synth('temp__1' : 1004), Synth('temp__2' : 1005) ]

(
o = a.collect { |synth, i|
	OSCFunc({ |msg|
		"Synth at index % threshold triggered at %\n"
		.postf(i, msg[3]);
	}, '/thresh', s.addr, argTemplate: [synth.nodeID]);
};
)

Synth at index 0 threshold triggered at -0.4969796538353
Synth at index 2 threshold triggered at 0.50061517953873
Synth at index 1 threshold triggered at 0.0011558956466615
Synth at index 0 threshold triggered at -0.49438661336899
Synth at index 2 threshold triggered at 0.50148421525955

a.do(_.free); o.do(_.free);

You can also have two or more SendReply-s in the same synth, even with the same command path, and filter them based on replyID.

arrayOfSignals.do { |channel, i|
	SendReply.kr(channel > threshold, '/thresh', channel, i)
};

… and argTemplate: [synth.nodeID, channelIndex].

hjh

2 Likes

Thanks a lot for this very interesting discussion, I’m learning lot.

It’s important here to understand the division of labor between the language and the audio server.

I see, it makes sense, I though all this code was run on the server here, I did not realize the OSC message where from the server to the client, I thought it was local to the server. Is there a way to “debug” to see what code/osc messge is running where? For instance, if I use a Condition in a synth, will it send and OSC messge to the client ?

I’d say the critique is rather exaggerated

Ahah yes, I’m just trying to understand what’s the cleanest solution and the various implications, but this is not that bad sure.

they could execute remote code in SC only if you receive a string over OSC and your code .interpret-s it

Well this assumes that the server does not contain any security flows, buffer overflow-like attacks can allow arbitrary code execution on innocent looking code… (countless attacks are done this way) but I’m working in cryptography so I’m maybe a bit too paranoid here (like most cryptographers) and I just like to avoid unecessary attack vectors. Actually, if I don’t specify an IP adress in the OSC commands, will it execute messages coming from any IP address or is it only allowing the server by default?

Name conflicts are not hard to manage, and as you correctly found, a one shot OSCFunc avoids them entirely.

I guess I can add in front of all OSC path a fixed string to create a kind of namespace yes, combined with argTemplate it is indeed quite unlikely to find collision I guess. But I don’t how OSCFunc helps more than OSCDef in this case, both need to specify a hardcoded path.

I think NodeWatcher can be used in some cases:

Thanks, but I tried the same code without the NodeWatcher and I get exactly the same result (but onFree is indeed interesting in some cases). What is NodeWatcher doing here exactly?

In any case, the point that was missed in the concern over a “race condition” is that SendReply includes two numbers apart from the command path and the values – the node ID from which the message is being sent …

Oh, using nodeID as an identifier is a great idea, much simpler than my imagined workarounds involving random UUID creation. Thanks a lot for these nice examples and tricks! argTemplate makes the code even cleaner.

While going through the doc, I also found some related functions that may be useful:

  • SendTrig is like SendReplay but directly comes with some pre-chosen paths & messages (based on message ID)
  • Condition can sometimes be helpful to pause a thread until another thread does an action (see hang/unhang)
  • FreeSelf can be used to free a synth when its input goes above/below a threshold (e.g. when reverb is finished), and it can be combined with waitForFree to start another action when the sync has finished playing:
// From the documentation of Conditional:
(
SynthDef(\help, { |out|
    var mod = LFNoise2.kr(ExpRand(0.5, 2)) * 0.5;
    var snd =  mod * Blip.ar(Rand(200, 800) * (mod + 1));
    Out.ar(out, snd);
    FreeSelf.kr(mod < 0); // free the synth when amplitude goes below 0.
}).add;
)

(
fork {
    10.do {
        "started a synth".postln;
        Synth(\help).waitForFree;
        "This one ended. Wait a second,  I will start the next one.".postln;
        1.wait;
    };
    "This is it.".postln;
}
);

“Client vs Server” helps a bit.

SC language code only runs in the sclang client.

A SynthDef uses sclang code to build a graph of unit generators. Then this graph gets transmitted to the server, and the server evaluates the UGens.

Note that Max/MSP and Pure Data also split data flow between messages and signals, where in general you should connect message outlets to other objects’ message inlets (similar to sclang objects), and signal outlets to signal inlets (similar to UGens). In these patching environments, messages patched into signal inputs might be promoted to signals, or might not (compare synth.set in SC). Signals cannot be patched directly into message inputs at all. For that, you need a bridge object such as edge~ (MSP) or threshold~ (Pd) or snapshot~ (both). In SC, SendReply / OSCFunc is the bridge.

It’s meaningless to use a Condition in a SynthDef.

I think so. SC is a low value target. I would be very surprised if anyone ever tried to hack it.

Edit: Though it wouldn’t hurt to have a cryptographer have a look at SC’s, message receipt code :thinking:

Your comment was about OSCdef names, though, which are different from OSC command paths.

You’ll have the same problem with OSC in any other software; this is not a SC problem.

hjh

Thanks for these details!

Ahah the software is rarely the target, the target is the computer and the software a way to enter and infect the computer. When people found a way to inject a virus when running a simple video (see e.g. the attack against the widely used libavcodec https://security.stackexchange.com/a/264173/164072), the goal was not vlc/chrome/… but the computer running it ^^ And thinking that a software has little interest for attackers make it precisely a great target as people will not spend much energy to make it as secure as more visible audited applications.

Oh good point, but OSC path are also vulnerable to name conflicts, but if OSC can’t be avoided sure it is quite fundamental and easily mitigated with proper namespaces.

Anyway, except for NodeWatcher I think you have answered most of my questions. Thanks!

Sure. What I meant by low-value target is that audio programming is already a niche, and SC is a niche within a niche. There’s always a cost to developing an exploit. Millions/billions of people use libavcodec – if a fraction of a percent of attacks against it are successful, you’d get something. If a fraction of a percent of attacks against SC are successful, you’d get bupkus. It’s not worth it for hackers to invest in that.

I’m not saying that the probability of an exploit is exactly 0 – but I think it would be near 0.

The OSC command path is designed for that – /jh/something.

In SC, you could avoid network transmission of OSC by using the internal server:

Server.default = s = Server.internal;
s.boot;
s.makeGui;

But the IDE’s server status bar won’t work, because it depends on the server receiving /status requests – hence s.makeGui. And if the server crashes, it will take the language down with it. Otherwise it should be fine. The internal server doesn’t open a UDP port, and communication between the language and the server is direct, within the same process, so if it will make you feel better, that’s an option.

hjh

It registers the Node behind the scenes, so it’s the same thing. But I think you can explore other features of NodeWatcher too.

	register { arg assumePlaying = false;
		NodeWatcher.register(this, assumePlaying)
	}

	unregister {
		NodeWatcher.unregister(this)
	}

	onFree { arg func;
		var f = {|n, m|
			if(m == \n_end) {
				func.value(this, m);
				this.removeDependant(f);
			}
		};
		this.register;
		this.addDependant(f);
	}

I see, thanks!

Oh really interesting, thanks!

the problem with this is that the Synth has to know about the threshold, and the OSC function.

The ask was to watch a bus, so this works

b = Bus.audio(s, 1);

// this watches the bus, and calls the (original) OSC function when it reaches the threshhold (and could be a more elaborate test)
a = {
    var sig = In.ar(b,1);
    var threshold = (sig > 0.9);
    SendReply.ar(threshold, '/threshold', sig);
}.play;

// the above watcher will work with any Synth that writes to the bus 
c = {Out.ar(b, LFNoise1.ar(100));}.play;

You can’t always go in and modify a synth - and you certainly don’t want to have to do that to add some monitoring

I don’t know a similar way of solving the Done problem - maybe keep a register of which nodes are writing to which bus, and when the bus is silent free the nodes?

Not changing the existing Synths seems important to me

May not always be possible – if the SynthDef doesn’t expose an internal signal on a bus, then it’s not accessible from outside the Synth. I.e., if it’s undesirable to add a SendReply just for monitoring, then it may also be undesirable to add an Out just for monitoring.

As often is the case, there are a lot of possible variations on the basic approach. The point of forum answers is to point users in the right direction.

I think watching for the automatically-sent /n_end messages is the best way.

hjh

The original question was about monitoring a bus.

Any reusable synth will allow you to specify the output bus, which is all this needs.

Actually I should amend this – /n_end will handle the case when doneAction = 2. It doesn’t handle the case of watching for a Done condition while keeping the synth alive. For that, I think it would be necessary to write either SendReply in the SynthDef, or an extra Out for the Done signal. (A synth node will not have access to the Done status of a UGen belonging to a different node, except via a bus. … On second though, watching a bus for silence and then freeing nodes in response to that strikes me as more troublesome than letting the node free itself [since that mechanism already exists] and watching for /n_end… which is where a “keep-node-alive” solution would be distinct from the “node-end” case.)

The title of the thread says “bus,” but the first post says “bus/input”… I guess I read “input” as an input to some arbitrary UGen. In any case it seems that the thread has reached a satisfactory resolution.

hjh