Cut groups with patterns

I’m trying to implement “cut groups” with independent patterns. E.g. to simulate open/closed hihats etc. My spontaneous approach was this:

(
t = TempoClock.default;
t.tempo = 30/60;
s.waitForBoot{
	~cutGroup = Group.new;

	SynthDef(\testPM, {|freq, amp, gate = 1|
		var env, sig;
		env = EnvGen.kr(Env.asr(), gate, doneAction: 2);
		sig = SinOsc.ar(freq, SinOsc.ar(freq*2, 0, 2pi*env)) * amp * env;
		Out.ar(0, sig!2);
	}).add;

	x = Pbind(
		\cut, Pfunc({~cutGroup.release(0.001)}),
		\instrument, \testPM,
		\group, ~cutGroup,
		\note, -24,
		\amp, 0.1,
		\dur, Prand([Rest(0.25), 0.25, 0.25, 0.75, 0.125, 0.25], inf)
	).play(quant: 1);

	y = Pbind(
		\cut, Pfunc({~cutGroup.release(0.001)}),
		\instrument, \testPM,
		\group, ~cutGroup,
		\note, 24,
		\amp, 0.05,
		\dur, Prand([0.5, 0.5, 0.75, 0.25, 0.125], inf)
	).play(quant: 1);
};
)

And it kind of works in some way but gives Node not found messages. I suspect these are from the \gate, 0 messages generated by the events after \dur * \legato when ~cutGroup.release was already sent.
Is there a way to “filter” out the \cut Pfunc from Event.play? I tried different collect functions on the Pbind but couldn’t get it to work. Any ideas?

If you have synths that require a gate then I would probably use a mixer:

(
t = TempoClock.default;
t.tempo = 30/60;
s.waitForBoot{
  var mixer;
  var bus1 = Bus.audio(s);
  var bus2 = Bus.audio(s);
  
	SynthDef(\testPM, {|freq, amp, bus, gate = 1|
		var env, sig;
		env = EnvGen.kr(Env.asr(), gate, doneAction: 2);
		sig = SinOsc.ar(freq, SinOsc.ar(freq*2, 0, 2pi*env)) * amp * env;
		Out.ar(bus, sig);
	}).add;
  
  SynthDef(\mix, {|bus|
    Out.ar(0, In.ar(bus)!2);
  }).add;
  
  mixer = Synth(\mix, [bus: bus1]);
    
  Pbind(
    \cut, Pfunc({mixer.set(\bus, bus1)}),
    \instrument, \testPM,
    \bus, bus1,
    \note, -24,
    \amp, 0.1,
    \dur, Prand([Rest(0.25), 0.25, 0.25, 0.75, 0.125, 0.25], inf)
  ).play(quant: 1);
  
  Pbind(
    \cut, Pfunc({mixer.set(\bus, bus2)}),
    \instrument, \testPM,
    \bus, bus2,
    \note, 24,
    \amp, 0.05,
    \dur, Prand([0.5, 0.5, 0.75, 0.25, 0.125], inf)
  ).play(quant: 1);
};
)
1 Like

That’s correct. You can avoid the late messages with defining \type, \on in Pbind, which supresses the release messages.
Timing is also an issue to think about. Currently the release message is sent and performed immediately, which means that it is before normal latency of 0.2 secs and message time span is ignored. This might or might not be relavant in your use case, just keep it in mind. You could also delay the sending within Pfunc by using schedAbs.
Another option would be having a pattern dedicated to sending group releases.

1 Like

The problem with suppressing release messages is that the synth will only be stopped if the other synth plays, which probably isn’t what you want.

The mixer solution works fine in this case. Thanks! The only problem i have with that is that it might not be the nicest model of the “real world” scenario, where e.g. the hihat can’t be opened and closed at the same time. It would be nice to be able to use one or more groups as an analogy for a collection of sounds and rules for which ones cuts others etc that makes up an instrument.

Oh, i thought that the server latency was always taken into account within the pattern system? Is the Pfunc excluded from that?

Latency is taken into account in a certain sense: the instantiation of a new synth (in case of type note) or the setting of a running synth (in case of Pmono) happens server-side with latency because these messages are sent with a time stamp.

What’s defined in the Function of a Pfunc is performed immediately (at the time the new Event is generated), the release msg is sent without time stamp and executed server-side as soon as it has arrived.

My first remark on delaying was actually incomplete, scheduling the release message with latency from lang would be the lazy solution, bundling with latency the more exact one. See also

http://doc.sccode.org/Guides/ServerTiming.html

There’s a way of doing this (though I wouldn’t use groups), but it’s not particularly easy. If I have time I’ll post a solution.

@emp - as promised, here’s a solution that will work:

Pfset({
  ~super_play = Event.default.parent[\play];

  ~play = {
    var server;
    if(choke_node.notNil){
      if (choke_node.atFail(\isPlaying, false)) {
        server = ~server ?? { Server.default };
        server.sendBundle(server.latency, ["/n_set"] ++ choke_node[\id] ++ ["gate", 0]);
      }
    };
    ~super_play.();

    choke_node = currentEnvironment;
  };
}, 
	Ppar([Pbind(
		\instrument, \testPM,
		\note, -24,
		\amp, 0.1,
		\dur, Prand([Rest(0.25), 0.25, 0.25, 0.75, 0.125, 0.25], inf)
	), Pbind(
		\instrument, \testPM,
		\note, 24,
		\amp, 0.05,
		\dur, Prand([0.5, 0.5, 0.75, 0.25, 0.125], inf)
)])).play(quant: 1)
)
1 Like

Ideally there would be a way to do this globally with a choke group value. I think I know how to do that, but the way events, patterns and parent environments work is confusing me at the moment.

The end game being to add the parameter ~choke_group to the default event type.

Modify the default event to support choke groups. This was a learning experience, even if the final solution should have been really obvious:

(
Event.parentEvents.default.put(\play, ({
  var chokeEnv = ();
  var super_play = Event.parentEvents.default[\play];

  {
    var chokeName = ~choke;
    if(chokeName.isNil.not){
      var choke_event = chokeEnv[("choke_" ++ chokeName).asSymbol];
      
      if(choke_event.isNil.not){
        var server = ~server ?? { Server.default };
        if (choke_event.atFail(\isPlaying, false)) {
          server.sendBundle(server.latency, ["/n_set"] ++ choke_event[\id] ++ ["gate", 0]);
        };
      };
      chokeEnv.put(("choke_" ++ chokeName).asSymbol, currentEnvironment);

    };
    super_play.();


}}).value();
);

SynthDef(\testPM, {|freq, amp, gate = 1|
		var env, sig;
		env = EnvGen.kr(Env.asr(), gate, doneAction: 2);
		sig = SinOsc.ar(freq, SinOsc.ar(freq*2, 0, 2pi*env)) * amp * env;
		Out.ar(0, sig!2);
	}).add;

)

So the basic idea here is that I replace the play function for the default event with my modified play function. To make this work I create a closure which contains the old play function (so I can call it - this is a form of OO overloading) and a dictionary for storing data about playing synths that are in choke groups.

If an event contains a choke group (where ~choke is the name of this particular choke group) then the modified play method checks to see if it has an event for an already playing synth in that choke group, and if it finds one it sends a gate message to that synth to stop it. The new synth is then added to the dictionary for this choke group.

2 Likes

Thanks, I was looking for something exactly like this (I was working on a similar solution using a gate argument – except sent to Groups, similar to @emp’s original approach), though maybe not as elegant as changing the default play method, which doesn’t require Group management). However, with your solution (@cian) I still get those “FAILURE IN SERVER /n_set Node NNNN not found” messages. @dkmayer’s trick mentioned above (to use \type, \on) seems to get rid of most of these messages, but then it becomes possible to get notes that are “stuck on”, when you call stop on the pattern player.

Thanks to everyone and sorry for replying to an old topic.

without having a ook at the specific examples, there’s an easy fix for this: use a group and free/release the group together with stopping.

g = Group.new;
...
p = Pbind(
   \group, g
   ...
).play
...

p.stop;
g.free;

Okay, that’s closer to what I was trying to do anyhow. Is there a good way to manage multiple Groups dynamically within a parent Group (e.g. in a NodeProxy)? For example, I’m playing the events/synths for a “drum machine” inside a NodeProxy, and for some of the events (e.g. bass or snare drum) I want them all to have their own “cut group”. For some others (e.g. open or closed hi-hat) I want several “notes” to share a “hi-hat” Group. I can manage this logic, but the dynamic creation of Groups as needed within another Group seems more complicated…I want something like: “create a ‘named’ Group if it doesn’t exist, otherwise use the one that exists already within this parent Group”. I can’t just put the Groups globally under the default Server Group because of ordering reasons, and because there might be different instances of my “drum machine” playing at once, in a live coding setup, so the “cutting” should be local to a parent Group scope.

Thanks.

P.S. I wondered if a “Voicer” (like the one in ddwLib) of some sort might be another approach, with the number of voices set to 1.

Yes, such a bookkeeping should certainly be doable. E.g., define an IdentityDictionary for the groups and write a constructor Function that does what you decribe (“exist” would mean value of that key is not nil).

Something was bothering me about cian’s earlier approach, to override the play function for all event types. Finally I realized what it was: You can’t assume that every event type is manipulating nodes in the server. So it really needs to be implemented in a more encapsulated way.

It could be an event type, but if you’re using \on to avoid premature releases, then you also need the pattern to clean up nodes upon stop. Pfset is a way to hack this, because it lets you attach a custom function to the player’s EventStreamCleanup; a new pattern class could make that interface more pleasant.

But an even simpler approach might be to gate the synth, and include an expression in the Pbind to guarantee that sustain is always less than dur. (Though this would be hard to integrate with Rest events.)

hjh