Cut groups with patterns


#1

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?


#2

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);
};
)

#3

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.


#4

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.


#5

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.


#6

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


#7

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


#8

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.


#9

@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)
)

#10

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.


#11

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.