Impulse.ar(0) vs. Trig1.ar(\trig.tr(1), SampleDur.ir)

hey, could someone explain to me the difference here? thanks :slight_smile:

Im creating two initial triggers, one with initTrigA = Impulse.ar(0); and the other with initTrigB = Trig1.ar(\trig.tr(1), SampleDur.ir);, they look the same on the plot. Then i trigger two one-shot ramps and derive a trigger from them via HPZ1.ar(oneShotPhaseA > 0) > 0 and HPZ1.ar(oneShotPhaseB > 0) > 0.
Why is the result for initTrigA and initTrigB the same, but the derived triggers are not?

(
{
	var initTrigA = Impulse.ar(0);
	var oneShotPhaseA = Sweep.ar(initTrigA, 1 / \duration.kr(0.02)) * (PulseCount.ar(initTrigA) > 0);
	var derivedTrigA = HPZ1.ar(oneShotPhaseA > 0) > 0;

	var initTrigB = Trig1.ar(\trig.tr(1), SampleDur.ir);
	var oneShotPhaseB = Sweep.ar(initTrigB, 1 / \duration.kr(0.02)) * (PulseCount.ar(initTrigB) > 0);
	var derivedTrigB = HPZ1.ar(oneShotPhaseB > 0) > 0;

	[initTrigA, derivedTrigA, initTrigB, derivedTrigB];

}.plot(0.00041);
)

additionally interesting, compare these two:

Sweep (from first example)

(
var rampOneShotA = { |trig, duration|
	var hasTriggered = PulseCount.ar(trig) > 0;
	var phase = Sweep.ar(trig, 1 / duration).clip(0, 1);
	phase.wrap(0, 1) * hasTriggered;
};

{
	var initTrigA = Impulse.ar(0);
	var oneShotPhaseA = rampOneShotA.(initTrigA, \duration.kr(0.02));
	var derivedTrigA = HPZ1.ar(oneShotPhaseA > 0) > 0;

	var initTrigB = Trig1.ar(\trig.tr(1), SampleDur.ir);
	var oneShotPhaseB = rampOneShotA.(initTrigB, \duration.kr(0.02));
	var derivedTrigB = HPZ1.ar(oneShotPhaseB > 0) > 0;

	[initTrigA, derivedTrigA, initTrigB, derivedTrigB];

}.plot(0.00041);
)

accum with Duty

(
var rampOneShotB = { |trig, duration|
	var hasTriggered = PulseCount.ar(trig) > 0;
	var accum = Duty.ar(SampleDur.ir, trig, Dseries(0, 1)) * hasTriggered;
	var slope = (1 / duration) * SampleDur.ir;
	(slope * accum).clip(0, 1).wrap(0, 1);
};

{
	var initTrigA = Impulse.ar(0);
	var oneShotPhaseA = rampOneShotB.(initTrigA, \duration.kr(0.02));
	var derivedTrigA = HPZ1.ar(oneShotPhaseA > 0) > 0;

	var initTrigB = Trig1.ar(\trig.tr(1), SampleDur.ir);
	var oneShotPhaseB = rampOneShotB.(initTrigB, \duration.kr(0.02));
	var derivedTrigB = HPZ1.ar(oneShotPhaseB > 0) > 0;

	[initTrigA, derivedTrigA, initTrigB, derivedTrigB];

}.plot(0.00041);
)

I remember from earlier that Sweep.ar() and Sweep.ar(Impulse.ar(0)) behave differently, can’t remember exactly which is which but as I recall one of the versions is delayed an extra sample. Not sure, but I think one was 2 (or 3) samples delayed and the other 3 (or 4) samples delayed. Not sure it helps here…

When im using Sweep triggered by Trig1.ar(\trig.tr(1), SampleDur.ir) the initial trigger for the measure ramp and the subdivided ramps are not aligned with the one-shot ramp.

(
var rampOneShot = { |trigIn, duration|
	var trig = Trig1.ar(trigIn, SampleDur.ir);
	var hasTriggered = PulseCount.ar(trig) > 0;
	var phase = Sweep.ar(trig, 1 / duration).clip(0, 1);
	phase.wrap(0, 1) * hasTriggered;
};

var oneShotRampToMeasureTrigger = { |phase|
	var compare = phase > 0;
	var delta = HPZ1.ar(compare);
	delta > 0;
};

var oneShotRampToStepTrigger = { |measurePhase, stepsPerMeasure|
	var measureTrigger = oneShotRampToMeasureTrigger.(measurePhase);
	var measurePhaseStepped = (measurePhase * stepsPerMeasure).floor;
	var delta = HPZ1.ar(measurePhaseStepped);
	var stepTrigger = delta > 0;
	stepTrigger + measureTrigger;
};

{
	var stepsPerMeasure = 7;

	var measurePhase = rampOneShot.(\trig.tr(1), \sustain.kr(0.02));
	var measureTrigger = oneShotRampToMeasureTrigger.(measurePhase);

	var stepPhase = (measurePhase * stepsPerMeasure).wrap(0, 1);
	var stepTrigger = oneShotRampToStepTrigger.(measurePhase, stepsPerMeasure);

	[measurePhase, measureTrigger, stepPhase, stepTrigger];

}.plot(0.0041);
)

When im using \trig.tr(1) into Sweep directly, it looks correct on the plot (first trigger is an initial trigger and all the other triggers are perfectly aligned with the subdivision wraps). The reason for that is unclear to me.

(
// use trigIn = \trig.tr(1) directly into Sweep here
var rampOneShot = { |trigIn, duration|
	var trig = Trig1.ar(trigIn, SampleDur.ir);
	var hasTriggered = PulseCount.ar(trig) > 0;
	var phase = Sweep.ar(trigIn, 1 / duration).clip(0, 1);
	phase.wrap(0, 1) * hasTriggered;
};

var oneShotRampToMeasureTrigger = { |phase|
	var compare = phase > 0;
	var delta = HPZ1.ar(compare);
	delta > 0;
};

var oneShotRampToStepTrigger = { |measurePhase, stepsPerMeasure|
	var measureTrigger = oneShotRampToMeasureTrigger.(measurePhase);
	var measurePhaseStepped = (measurePhase * stepsPerMeasure).floor;
	var delta = HPZ1.ar(measurePhaseStepped);
	var stepTrigger = delta > 0;
	stepTrigger + measureTrigger;
};

{
	var stepsPerMeasure = 7;

	var measurePhase = rampOneShot.(\trig.tr(1), \sustain.kr(0.02));
	var measureTrigger = oneShotRampToMeasureTrigger.(measurePhase);

	var stepPhase = (measurePhase * stepsPerMeasure).wrap(0, 1);
	var stepTrigger = oneShotRampToStepTrigger.(measurePhase, stepsPerMeasure);

	[measurePhase, measureTrigger, stepPhase, stepTrigger];

}.plot(0.0041);
)

When beeing triggered by Pmono however the first version (incorrect on the plot) gives a steady clock pulse.

(
var rampOneShot = { |trigIn, duration|
	var trig = Trig1.ar(trigIn, SampleDur.ir);
	var hasTriggered = PulseCount.ar(trig) > 0;
	var phase = Sweep.ar(trig, 1 / duration).clip(0, 1);
	phase.wrap(0, 1) * hasTriggered;
};

var oneShotRampToMeasureTrigger = { |phase|
	var compare = phase > 0;
	var delta = HPZ1.ar(compare);
	delta > 0;
};

var oneShotRampToStepTrigger = { |measurePhase, stepsPerMeasure|
	var measureTrigger = oneShotRampToMeasureTrigger.(measurePhase);
	var measurePhaseStepped = (measurePhase * stepsPerMeasure).floor;
	var delta = HPZ1.ar(measurePhaseStepped);
	var stepTrigger = delta > 0;
	stepTrigger + measureTrigger;
};

SynthDef(\test, {
	
	var stepsPerMeasure = \stepsPerMeasure.kr(7);

	var measurePhase = rampOneShot.(\trig.tr(1), \sustain.kr(0.02));
	var measureTrigger = oneShotRampToMeasureTrigger.(measurePhase);

	var stepPhase = (measurePhase * stepsPerMeasure).wrap(0, 1);
	var stepTrigger = oneShotRampToStepTrigger.(measurePhase, stepsPerMeasure);

	Out.ar(\out.kr(0), stepTrigger);
}).add;
)

(
Pdef(\test,
	Pmono(\test,

		\trig, 1,
		\legato, 1.0,
		\stepsPerMeasure, 7,

		\time, Pfunc { |ev| ev.use { ~sustain.value } / thisThread.clock.tempo },

	);
).play;
)

where the second (correct on the plot) gives an inconsistent clock pulse:

(
// use trigIn = \trig.tr(1) directly into Sweep here
var rampOneShot = { |trigIn, duration|
	var trig = Trig1.ar(trigIn, SampleDur.ir);
	var hasTriggered = PulseCount.ar(trig) > 0;
	var phase = Sweep.ar(trigIn, 1 / duration).clip(0, 1);
	phase.wrap(0, 1) * hasTriggered;
};

var oneShotRampToMeasureTrigger = { |phase|
	var compare = phase > 0;
	var delta = HPZ1.ar(compare);
	delta > 0;
};

var oneShotRampToStepTrigger = { |measurePhase, stepsPerMeasure|
	var measureTrigger = oneShotRampToMeasureTrigger.(measurePhase);
	var measurePhaseStepped = (measurePhase * stepsPerMeasure).floor;
	var delta = HPZ1.ar(measurePhaseStepped);
	var stepTrigger = delta > 0;
	stepTrigger + measureTrigger;
};

SynthDef(\test, {
	
	var stepsPerMeasure = \stepsPerMeasure.kr(7);

	var measurePhase = rampOneShot.(\trig.tr(1), \sustain.kr(0.02));
	var measureTrigger = oneShotRampToMeasureTrigger.(measurePhase);

	var stepPhase = (measurePhase * stepsPerMeasure).wrap(0, 1);
	var stepTrigger = oneShotRampToStepTrigger.(measurePhase, stepsPerMeasure);

	Out.ar(\out.kr(0), stepTrigger);
}).add;
)

(
Pdef(\test,
	Pmono(\test,

		\trig, 1,
		\legato, 1.0,
		\stepsPerMeasure, 7,

		\time, Pfunc { |ev| ev.use { ~sustain.value } / thisThread.clock.tempo },

	);
).play;
)

In these examples im not using the subdivided ramps directly so it doesnt matter that they are not perfectly aligned with the triggers but when i use them it does matter.

The difference is the pre-sample: Impulse.ar(0)'s presample is 1, while NamedControl.tr (or rather, its TrigControl) initializes a presample of 0.

There have been many, many subtle init bugs related to this. Impulse used to be one of them, but it’s been fixed. TrigControl hasn’t.

(
{ |t_t2 = 1|  // avoid warning in 'plot'
	var t1 = Impulse.kr(0);
	[t1, Lag.kr(t1, 0.4), t_t2, Lag.kr(t_t2, 0.4)]
}.plot(0.5);
)

t1’s Lag starts at 1 and decays; t2’s Lag starts at 0 (pre-sample) and doesn’t have much time to ramp up before decaying.

My view is that every UGens’s Ctor should initialize outputs to y0 and never y(-1). TrigControl is initting to y(-1) – the 0 before the trigger – and (as is common in that case) this causes otherwise incomprehensible bugs.

hjh

thanks for the explanation. Whats about Sweep in this context, does it behave correctly and the issue ist just to be found with Impulse vs. TrigControl ?

Im just wondering because it behaves differently then accum with Duty (see second post). What is the expected initial value of Sweep when beeing triggered?

As far as i understand it:
HPZ1 calculates the delta by current sample - last sample.

When beeing implemented as HPZ1.ar(oneShotPhaseB > 0) > 0 i get an initial trigger if the presample of Sweep is <= 0, or?

If you’re going to dig this deep into very specific UGen initialization behavior, then it would be worth your time to get a little more familiar with the UGen source code. In most cases, the C idiom isn’t particularly difficult to read.

So, for example, I search the plug-ins directory for Sweep (I specifically use emacs’ “rgrep” command, but you could use any tool), and it’s found in TriggerUGens.cpp.

Here, at the bottom of Sweep_Ctor (the initialization function), there’s:

    unit->m_previn = ZIN0(0);

IMO this should be 0.f but strangely, the UGen still seems to work.

And there’s:

ZOUT0(0) = unit->mLevel = 0.f;

So any UGens downstream from Sweep will always receive zero from upstream during init (which you would expect, since its help file advertises that the ramp begins from 0).

I’ll be honest here that my time today is quite limited, and the questions you’re asking would involve a lot of testing and source code review to answer correctly. So I’m afraid I can’t get that much involved at this time. (Even just looking at Sweep, I don’t understand why it begins an upward ramp even in the absence of an initial trigger … that is, what perhaps seems on your side like a simple, innocent question turns into at least an hour, maybe two, on my side to look at properly… ain’t gonna happen this morning.) But maybe these couple of hints will help you get more information from the SC sources.

hjh

1 Like

thanks alot for taking some time :slight_smile:

Ive looked up Sweep (TriggerUgens.cpp) and Impulse (LFUgens.cpp).

Probably it isnt too bad that Sweep does set unit->m_previn = ZIN0(0);
If you pass in an initial trigger with Impulse.ar(0) you get out an initial trigger with HPZ1.ar
The first value of Sweep should be zero but shouldnt the pre-initial value be 1 (one sample before the wrap) ?.

(
var rampOneShot = { |trig, duration|
	var hasTriggered = PulseCount.ar(trig) > 0;
	var phase = Sweep.ar(trig, 1 / duration).clip(0, 1);
	phase.wrap(0, 1) * hasTriggered;
};

{ |t_trig = 1, duration = 0.02|

	var initTrig = Impulse.ar(0);
	var oneShotPhase = rampOneShot.(initTrig, duration);
	var derivedTrig = HPZ1.ar(oneShotPhase > 0) > 0;

	[initTrig, derivedTrig];
}.plot(0.00041);
)

But this implementation for Sweep does lead to inconsistent behaviour when using NamedControl.tr, which doesnt give an initial trigger. Using NamedControl.tr is AFAIK the only way to trigger from the language assuming a use of Pmono / Synth.set.
I couldnt find a .cpp file with TrigControl, where is this defined?

I asked a similiar question on the gen~ discord:
When you calculate the delta to get a trigger from a phasors wrap in gen~, would the first trigger when your phasor starts from 0 be an “initial trigger” (picture 1) or one sample late from the phasors wrap (picture 2)? Or in other words does the phasor object has a pre-initial value of 1?



The answer was: The delta op assumes an initial state of zero. So the very 1st sample output of delta will be equal to the input value.

did some hacks :smiley:

Sweep triggered by Impulse starts at a sample count of 1 instead of 0:

(
{ |t_trig = 1, duration = 0.02|
	
	var initTrigA = Impulse.ar(0);
	var oneShotPhaseA = Sweep.ar(initTrigA, SampleRate.ir) * (1 / duration * SampleDur.ir) * (PulseCount.ar(initTrigA) > 0);

	var initTrigB = Trig1.ar(t_trig, SampleDur.ir);
	var oneShotPhaseB = Sweep.ar(initTrigB, SampleRate.ir) * (1 / duration * SampleDur.ir) * (PulseCount.ar(initTrigB) > 0);
	
	[oneShotPhaseA, oneShotPhaseB];

}.plot(0.00041);
)

if you add 1 sample when beeing triggered with Trig1, you get an inital trigger in and an inital trigger out.

(
{ |t_trig = 1, duration = 0.02|
	
	var initTrigA = Impulse.ar(0);
	// do nothing!
	var oneShotPhaseA = Sweep.ar(initTrigA, SampleRate.ir) - 0 * (1 / duration * SampleDur.ir) * (PulseCount.ar(initTrigA) > 0);
	var derivedTrigA = HPZ1.ar(oneShotPhaseA > 0) > 0;
	
	var initTrigB = Trig1.ar(t_trig, SampleDur.ir);
	// add 1!
	var oneShotPhaseB = Sweep.ar(initTrigB, SampleRate.ir) + 1 * (1 / duration * SampleDur.ir) * (PulseCount.ar(initTrigB) > 0);
	var derivedTrigB = HPZ1.ar(oneShotPhaseB > 0) > 0;
	
	[initTrigA, derivedTrigA, initTrigB, derivedTrigB];

}.plot(0.00041);
)

and when subtracting 1 for the one beeing triggered with Impulse, you get an initial trigger in and a trigger half a sample later out:

(
{ |t_trig = 1, duration = 0.02|
	
	var initTrigA = Impulse.ar(0);
	// subtract 1!
	var oneShotPhaseA = Sweep.ar(initTrigA, SampleRate.ir) - 1 * (1 / duration * SampleDur.ir) * (PulseCount.ar(initTrigA) > 0);
	var derivedTrigA = HPZ1.ar(oneShotPhaseA > 0) > 0;
	
	var initTrigB = Trig1.ar(t_trig, SampleDur.ir);
	// do nothing!
	var oneShotPhaseB = Sweep.ar(initTrigB, SampleRate.ir) + 0 * (1 / duration * SampleDur.ir) * (PulseCount.ar(initTrigB) > 0);
	var derivedTrigB = HPZ1.ar(oneShotPhaseB > 0) > 0;
	
	[initTrigA, derivedTrigA, initTrigB, derivedTrigB];

}.plot(0.00041);
)

I think the trig here means a trigger for resetting – so not for starting, but for ending, so to say.

This makes sense, but sweep shouldn’t start from sample count one. See examples above. That’s a bug imo.

of course not, that should not be.

I will raise an issue on GitHub. I think it would also be good if the trig argument would be renamed to reset.