Trigger cooldown on all triggers if one trigger fired

I have 8 triggers of which all fire depending on certain logical conditions. All of the conditions do not have any overlap so they cannot fire at the same time, but they can fire very shortly after eachother. We are talking about in-between-periods smaller than 100 samples. What i am trying to achieve is that if one of the triggers fires, all triggers (including the one that just fired) will not be registered for a set amount of time. I know this this possible with TDelay which filters out any incoming triggers between the input and the delayed output. But the problem is that i cant have a delay bigger than 0.01 seconds, but i do need a relax time of bigger than 0.01 seconds.

trig1 = Trig1.ar(crossBeginCond & chanCond1, 0.01);
trig2 = Trig1.ar(crossBeginCond & chanCond2, 0.01);

trig3 = Trig1.ar(curHeadCond & chanCond1, 0.01);
trig4 = Trig1.ar(curHeadCond & chanCond2, 0.01);

trig5 = Trig1.ar(exception1 & chanCond1, 0.01);
trig6 = Trig1.ar(exception1 & chanCond2, 0.01);

trig7 = Trig1.ar(exception2 & chanCond1, 0.01);
trig8 = Trig1.ar(exception2 & chanCond2, 0.01);
        
// if im not mistaken this realizes a trigger cooldown time of 0.01
// i want a bigger relax time but a delay of at most 0.01
// t_outx only fires when trigx was the first of all triggers to fire
// this works because none of the trigx's can fire at the same time
// i hope this is correct
t_out1 = TDelay.ar(trig1, 0.01) * TDelay.ar(trig1 + trig2 + trig3 + trig4 + trig5 + trig6 + trig7 + trig8, 0.01);
t_out2 = TDelay.ar(trig2, 0.01) * TDelay.ar(trig1 + trig2 + trig3 + trig4 + trig5 + trig6 + trig7 + trig8, 0.01);
t_out3 = TDelay.ar(trig3, 0.01) * TDelay.ar(trig1 + trig2 + trig3 + trig4 + trig5 + trig6 + trig7 + trig8, 0.01);
t_out4 = TDelay.ar(trig4, 0.01) * TDelay.ar(trig1 + trig2 + trig3 + trig4 + trig5 + trig6 + trig7 + trig8, 0.01);
t_out5 = TDelay.ar(trig5, 0.01) * TDelay.ar(trig1 + trig2 + trig3 + trig4 + trig5 + trig6 + trig7 + trig8, 0.01);
t_out6 = TDelay.ar(trig6, 0.01) * TDelay.ar(trig1 + trig2 + trig3 + trig4 + trig5 + trig6 + trig7 + trig8, 0.01);
t_out7 = TDelay.ar(trig7, 0.01) * TDelay.ar(trig1 + trig2 + trig3 + trig4 + trig5 + trig6 + trig7 + trig8, 0.01);
t_out8 = TDelay.ar(trig8, 0.01) * TDelay.ar(trig1 + trig2 + trig3 + trig4 + trig5 + trig6 + trig7 + trig8, 0.01);

LocalOut.ar([t_out1, t_out2, t_out3, t_out4, t_out5, t_out6, t_out7, t_out8]);

I can delay everything in the synthdef by the wanted relaxtime, and then set TDelay to 0.01 + relaxtime. This way the trigger delay time relative to the rest of the synthdef is not bigger than 0.01. But i’d like to avoid this, since this complicates things and reduces responsiveness.
Is there anything i can do?

There is a tricky feedback problem here.

It’s easy to calculate whether any of the underlying conditions has been true within the last ‘x’ seconds.

But a condition may be true, and its trigger may be suppressed, based on whether any other trigger has or has not been suppressed… but whether these triggers have been suppressed depends on whether the first trigger has been suppressed… which depends on whether the other triggers have been suppressed… ad infinitum.

… which implies a blocksize delay = ControlDur.ir. This will be an order of magnitude smaller than 0.01.

But… if I’m reading your requirement right, you have an infinitely recursive definition, which simply can’t be done. Either I’ve misunderstood, or you might have to change the concept.

The good news is that Trig1 itself can act as its own “relax time.”

It might be like this, but I’m not sure I have all the information required to test it myself:

var trigFeedback = LocalIn.ar(8);

var trigs = Trig1.ar([
	crossBeginCond & chanCond1,
	crossBeginCond & chanCond2,
	...
], relaxTime);  // you decide relaxTime

var someTriggerIsOpen = (trigFeedback > 0)  // flatten to 0 or 1
.reduce('||');  // get the OR of all of them

var trig_out = TDelay.ar(trigs, ControlDur.ir) * (someTriggerIsOpen <= 0);

...

LocalOut.ar(trig_out);

… but it occurs to me that this might not work, because TDelay introduces a block delay, and LocalOut / LocalIn adds another block delay, but the logic is based on a single block delay. That’s a reflection of the “infinitely recursive” definition that I mentioned above… there will always be some self-contradiction when trying to resolve that.

hjh

Thanks, i didnt know that trig1 works like that! Maybe instead of letting actions depend on the circumstances, i can try to force the circumstances. Just like i did with my first attempt. Its only the question if thats possible in this case…

I think i found a solution.

// in the first part a particular trigger prevents itself, after firing, to fire again within the relaxtime
trig1 = Trig1.ar(crossBeginCond & chanCond1, relaxTime);
trig2 = Trig1.ar(crossBeginCond & chanCond2, relaxTime);

trig3 = Trig1.ar(curHeadCond & chanCond1, relaxTime);
trig4 = Trig1.ar(curHeadCond & chanCond2, relaxTime);

trig5 = Trig1.ar(exception1 & chanCond1, relaxTime);
trig6 = Trig1.ar(exception1 & chanCond2, relaxTime);

trig7 = Trig1.ar(exception2 & chanCond1, relaxTime);
trig8 = Trig1.ar(exception2 & chanCond2, relaxTime);

// here every t_out_i contains every trigger in the Trig1 argument except itself: trig_i
// if trig_i was not the first one to fire, then there exists a trigger
// lets say trig_j that was the first one
// trig_j is contained in t_out_i's Trig1 argument, and so it fired the Trig1 ugen of t_out_i
// which results in a forced value of 0 for the relaxTime amount of time
// because of the linlin(0, 1, 1, 0) mapping

// if trig_i was the first one to fire, then because trig_i is not contained 
// in t_out_i 's Trig1 argument, the Trig1 ugen is 1 from the begin for a certain amount of time
// this is guaranteed because none of the triggers can fire at the same time
// then t_out_i = 1 * 1 for that small amount of time and the trigger has been registered

t_out1 = trig1 * Trig1.ar(trig2 + trig3 + trig4 + trig5 + trig6 + trig7 + trig8, relaxTime).linlin(0, 1, 1, 0);
t_out2 = trig2 * Trig1.ar(trig1 + trig3 + trig4 + trig5 + trig6 + trig7 + trig8, relaxTime).linlin(0, 1, 1, 0);
t_out3 = trig3 * Trig1.ar(trig1 + trig2 + trig4 + trig5 + trig6 + trig7 + trig8, relaxTime).linlin(0, 1, 1, 0);
t_out4 = trig4 * Trig1.ar(trig1 + trig2 + trig3 + trig5 + trig6 + trig7 + trig8, relaxTime).linlin(0, 1, 1, 0);
t_out5 = trig5 * Trig1.ar(trig1 + trig2 + trig3 + trig4 + trig6 + trig7 + trig8, relaxTime).linlin(0, 1, 1, 0);
t_out6 = trig6 * Trig1.ar(trig1 + trig2 + trig3 + trig4 + trig5 + trig7 + trig8, relaxTime).linlin(0, 1, 1, 0);
t_out7 = trig7 * Trig1.ar(trig1 + trig2 + trig3 + trig4 + trig5 + trig6 + trig8, relaxTime).linlin(0, 1, 1, 0);
t_out8 = trig8 * Trig1.ar(trig1 + trig2 + trig3 + trig4 + trig5 + trig6 + trig7, relaxTime).linlin(0, 1, 1, 0);

Sorry for the long comment, i hope the proof is correct

I had another brainstorm about this, just before getting up.

Another way to handle the relax time is to start a timer when a trigger passes through. As long as this timer is < relaxTime, then triggers should be suppressed.

That is, the condition to pass a trigger through is timer > relaxTime. This can be implemented without the infinite recursion!

Feedback enters into it because the condition to restart the timer is: the OR of all triggers AND (timer > relaxTime) – meaning that the timer’s value depends on itself. That requires a LocalIn / LocalOut feedback loop. The fed-back timer value will be one block duration less than the true timer value, but we can account for that in the condition.

BTW the code style gets a lot easier to handle if you use arrays.

var trigs = [
	crossBeginCond & chanCond1,
	crossBeginCond & chanCond2,
	
	curHeadCond & chanCond1,
	curHeadCond & chanCond2,
	
	exception1 & chanCond1,
	exception1 & chanCond2,
	
	exception2 & chanCond1,
	exception2 & chanCond2
];

// in fact, `trigs` can be written with even less redundancy:
// var trigs = [crossBeginCond, curHeadCond, exception1, exception2]
// &.x [chanCond1, chanCond2];

// OR all of the triggers
var anyTrig = trigs.reduce('|');

var sweepFeedback = LocalIn.ar(1);

var sweep = Sweep.ar(
	// because sweepFeedback is "ControlDur.ir" less than true
	Delay1.ar(anyTrig * (sweepFeedback > (relaxTime - ControlDur.ir))),
	1
);

var reducedTrigs = trigs * (sweep > relaxTime);

LocalOut.ar(sweep);

^^ I haven’t tested this but I think there’s a pretty good chance it might work. You might get spurious triggers if two triggers occur within one control block. Perhaps look into the Feedback quark to replace LocalIn/Out with a true one-sample feedback loop.

EDIT: Added Delay1 because the Sweep reset would suppress all triggers!

hjh

My quick solution to this looks like:

(

{
    var inputs, trigs, cooldown;
    
    inputs = { Dust.ar(2000) } ! 8;
    
    cooldown = \cooldown.kr(1) - SampleDur.ir;  // account for my 1 sample delay later
    
    cooldown = Trig.ar(inputs.sum, cooldown);   // have we recieved ANY trigger?
    cooldown = cooldown <= 0;                   // Invert my trigger, I want to let other things through when I *haven't* heard anything for a while
    cooldown = Delay1.ar(cooldown);             // I only care about muting things AFTER my first trigger
    
    trigs = Trig.ar(inputs * cooldown, 0);
    
    (Decay.ar(trigs, 4) * LFSaw.ar((100,200..900))).sum * [1, 1];
}.play
        
)

This is a little simpler than the other proposed solutions, so I wonder if I’m missing part of desired behavior? This also has the assumptions:

  1. inputs are already in single-sample-trigger format - so another step is needed to turn incoming control values into triggers.
  2. This assumes (based on the OP) that input triggers are guaranteed to not happen at the same time (de-duplicating these would take another layer)

@jamshark70 thanks again! Im honoured that someone thinks about my problems in bed.
@scztt If i listen it seems to work, but i dont understand why it works. You delay the cooldown, which results in a set of 0 values being delayed. So you know Delay1.ar(cooldown) == 0 after the 1 sample delay. But what is the value of Delay1.ar(cooldown) during that 1st 1 sample period? Isnt it also equal to 0? So this would according to my theory also mute the first trigger. Yet i do hear triggers firing.

Also this maybe off-topic, but i have a hard time reading plots of triggers and impulses. Is there any tool that allows you to visually see values on a single-sample scale?

My interpretation of it is:

// sample 0 (first trigger at inputs[4], cooldown is N)
inputs[4] == 1
cooldown = Trig.ar([0, 0, 0, 1, 0, 0, 0, 0].sum, N) == 1
cooldown = Delay1.ar(cooldown) == 0
cooldown = cooldown <= 0 == 1
trigs[4] = Trig.ar(1 * cooldown) == 1

// sample 1 (another trigger at inputs[6])
inputs[6] == 1
cooldown = Delay1.ar(cooldown) == 1 // our 1 from the previous sample
cooldown = cooldown <= 0 == 0
trigs[6] = Trig.ar(1 * cooldown) == 0

This is why I invert the trigger, cooldown = cooldown <= 0 - in it’s untriggered state, I want it to let triggers through - once it’s been triggered, I want it to be 0 to mute any other incoming triggers.

You should be able to use { ... }.plot, as long as you do it for a small enough duration that you can see individual samples. e.g.:

(
{
    Dust2.ar([
        1000, 1000, 1000
    ]) > 0
}.plot(600 / s.sampleRate, separately: true)
)

In your first post you had the inversion before the delay. Idk if that matters. But with the inversion after the delay it makes more sense to me, thanks.

I was thinking perhaps that Trig and Trig1 would reset their timers if a trigger arrives in the middle of an “open” duration. If that were the case, it would be difficult.

But @scztt is correct – Trig(1) ignores triggers that arrive while the timed trigger is open:

(
{
	var freq = 500;
	var pulse = Impulse.ar(freq);
	var dur = 1.25 / freq;
	[pulse, Trig1.ar(pulse, dur)]
}.plot;
)

So scztt’s approach looks right to me, and is better than what I suggested (by being simpler and more direct).

hjh