Riddle: how to free synths via control ids

Here is a riddle I struggle with. Maybe some of you has an idea of how to solve it. The basic idea is to free synths not via their nodeID, but via some id, and a group.set message to their group. The motivation is:

  • the synths are grains, so no bookkeeping of nodeIDs in sclang should be necessary
  • the groups are used to keep node order
  • synths may need to be freed together that are in different groups

So I keep all synths in a large global group and send the id message to it. The synth knows how to free itself if the id matches with its own.

The problem is the following: when two group.set messages happen within one control block, they override each other before the synth is freed.

All this happens in SuperDirt, but here is a minimal example:


(
SynthDef(\test, { |id, gate_id = -1, freq = 440|
	var sound = SinOsc.ar(freq);
	var env = FreeSelf.kr(absdif(id, gate_id) < 0.5);
	Out.ar(0, sound * 0.03)
}).add
)

// free it by setting the gate id.
k = Synth.new(\test, [\id, 4])
k.set(\gate_id, 4);


// now more synths

(
g = {
var group = Group.new;
4.do { |i|
	5.do { |j|
		var freq = [300, 700, 2000, 3000].at(i) * 4.0.rand2.midiratio;
		var id = [4, 5, 6, 7].at(i);
		Synth.grain(\test, [\id, id, \freq, freq], group)
	}
};

{ |dt = 0.01|
	fork {
		[4, 5, 6, 7].do { |id|
			group.set(\gate_id, id);
			dt.wait;
		};
	}
};
};
)

f = g.value;
f.(0.7);

f = g.value;
f.(0.0001); // some notes hang

Does anyone have a trick of how to handle this situation? It seems to be a fudamental limitation of scsynth that comes from the double use of groups as messaging relay and node order scheme.

If you have a relatively small pool of id’s, why not associate a Bus with every id, and then send a control value change to that Bus when you want to signal that group?

~ids = 16.collect {
  Bus.audio(s, 1);
};

~groups = 4.collect {
  Group()
};

~nodes = 100.collect {
     Synth(\test, args:[\idInput, ~ids.choose.asMap], target:~groups.choose)
};

~ids[3].set(1);

It’s a bit more complex if you want to send single-sample triggers to the Synths, but that should be as easy as a Synth that outputs a single trigger and then free’s?

1 Like

ah yes. Did you mean

~ids = 16.collect {
  Bus.control(s, 1);
};

?

Yeah, that works too :slight_smile:. I use audio buses / inputs for nearly everything these days.

1 Like

@scztt - do you have something installed allowing you to do .set on audio busses?

Hm, so when a new synth is started, the bus has to be zero again. So we need to set it back to zero after a very short time.

  • What will happen if in the intervening time, the bus is set to 1 again by some other process? Probably we need to work with free-synths that write outputs, which will add up.
  • What will happen if we start a new synth in this short period which shouldn’t be freed (because the free message was before?) Perhaps some well tuned initial grace period may help.

“no bookkeeping of nodeIDs in sclang should be necessary” looks like a hidden assumption – if synths are grouped together based on some criteria other than group membership, then bookkeeping of nodeIDs is a valid solution to the problem. Needing to free synths together, from the groups, at least calls the bookkeeping assumption into question. (It may be inconvenient and you’d rather not do it, but that’s different from saying “shouldn’t be necessary.”)

I was considering a group structure like this – if you have, say, 3 ordering groups and 4 release IDs, first make the ordering groups:

g = Array.fill(3, { Group(addAction: \addToTail) });

NODE TREE Group 0
   1 group
      1000 group
      1001 group
      1002 group

Then insert release groups:

h = Array.fill(4, {
	g.collect { |parent| Group(parent) }
});

NODE TREE Group 0
   1 group
      1000 group
         1012 group
         1009 group
         1006 group
         1003 group
      1001 group
         1013 group
         1010 group
         1007 group
         1004 group
      1002 group
         1014 group
         1011 group
         1008 group
         1005 group

(Sure, the release groups are “backward,” but I think this doesn’t matter because they aren’t used for ordering, and it’s easy to \addToTail if it’s uncomfortable to look at.)

To free release ID 0, you’d do h[0].do(_.release) and it would target groups 1003, 1004 and 1005.

Groups are CPU-cheap – using a lot of them to model a matrix of node-order x release ID is a valid way to go.

All of that could be automated by a class – agreed that it will be confusing to do by hand if there are a lot of groups, but this is exactly what object modeling is for – the object provides a nice interface and handles the dirty work for you.

(This is another hidden assumption – because we have so many examples in the help using Groups and Synths directly, we often assume that Groups and Synths in themselves are an overarching structure that should handle a wide variety of requirements elegantly. I think they are really resources to be aggregated – for some problems, the best solution is to use multiple nodes and manage them through a class or prototype. Perhaps another reason for this assumption is that we want to s.queryAllNodes and see a human-readable reflection of the DSP design, but that might not always be optimal.)

hjh

This is more like an open assumption. But of course one can also go down this path – I just find it very unattractive.

Yes, I agree that groups can be very useful in the way you suggest. This is the structure I originally had implemented – and it has one big advantage, namely that there are no problems with messages that override each other. The structural problem is (this is why it is somewhat of a riddle) that one synth can’t belong to several groups.

In my problem description I have simplified a bit. There is more than one criterium for freeing a synth. The actual case is here: SuperDirt/classes/SuperDirtUGens.sc at 5a3731dd1c5cd7b0233f69843169ae56cb1bf559 · musikinformatik/SuperDirt · GitHub

DirtGateCutGroup {

	*ar { | releaseTime = 0.02, doneAction = 2 |
		// this is necessary because the message "==" tests for objects, not for signals
		var same = { |a, b| BinaryOpUGen('==', a, b) };
		var or = { |a, b| (a + b) > 0 };
		var and = { |... args| args.product };
		var sameSample = same.(\sample.ir(0), \gateSample.kr(0));
		var sameCut = same.(\cut.ir(0), \gateCut.kr(0));
		var free = and.(or.(\cutAllSamples.kr(0), sameSample), sameCut);
		^EnvGen.kr(Env.cutoff(releaseTime), (1 - free), doneAction:doneAction);
	}
}

The sample control is a hash value of the sample. Theoretically, this would require numCutGroups x numSamples groups for each orbit, and in order to know which to free, sclang would have to traverse the whole tree for each cut.

I suspect we will disagree here (that’s OK).

The Group class is only a thin abstraction layer representing a group node in the server. Because of its name, we want to press it into service for other purposes, but in my opinion, this is a conceptual error.

Group simply is not a general-purpose node organizer. It’s a resource, which you can organize into higher-level behaviors. I think (again, my opinion) is that it’s easier to think in terms of organizing, on the client side, the given resources in the server, rather than to try to get the server resources to do things they were not designed to do (or to implement new server features).

Theoretically, this would require numCutGroups x numSamples groups for each orbit…

I was a bit concerned about scalability… there would be a point where the matrix gets too large to be practical.

and in order to know which to free, sclang would have to traverse the whole tree for each cut.

I think I was assuming a collection of groups would be keyed according to the cut group ID, which should eliminate some of the traversal? If the collection is organized according to the requirement, then it’s not “go check everything to see if it matches,” but “go directly to this part of the tree.”

In any case… a control input can’t have more than one value in one control cycle. So, using a single control name will not satisfy the requirement of releasing multiple cut groups at exactly the same time. So I think you’re left with:

  • Enforce a minimum wait time for successive cut triggers. (5 cut groups freeing within 8 ms shouldn’t be too bad.)
  • Or use a group matrix.
  • Or track node IDs belonging to cut groups.
  • Or uses buses as scztt suggests.

Of buses:

Hm, so when a new synth is started, the bus has to be zero again.

Could you use a synth to set the value for exactly one control period?

hjh

1 Like

Well, I have described only the trade-offs … I only wonder it might have been a mistake to use the same structure for two things:

  • node ordering
  • message dispatching

But anyway, this is where we are with scsynth, and the only solid alternative is to do the work on the sclang side. So yes, node bookkeeping is the only way, even at a performance cost (for dense grain clouds it doubles).

Thanks, this is a good overview.

  • spreading out the message timing is something I tried, it can become a little tedious, too. But it is an option, using a PriorityQueue.
  • a full group matrix would be too large
  • tracking nodeIDs is a performance penalty, but works
  • buses will make the problem less probable, but essentially shifts it only a bit. Still, no control can have two values per block. Audio rate buses would be safer, but still you could have rare cases of hanging notes.

unless someone comes with a smart trick – ugen commands maybe?

If you have one bus per cut group, then you could trigger any subset of them simultaneously.

hjh

Yes, that would work. But it still would require one bus per sample on top of this. Of course buses could be allocated on the fly, but it is undefined when to deallocate them again (that would require node tracking).

I think I am happy now with just tracking nodes that need a cut.

For anyone interested, it is here:

I am merging this now – I hope it is a good moment.