Initial trigger with Pulsedivider

hey, would someone know how i can get this pulsedivider attempt to output an initial trigger and also distribute the phases correctly accross the channels:

(
var multiChannelTrigger = { |numChannels, trig|
	var count = Demand.ar(trig, 0, Dseries(0, 1, inf));
	numChannels.collect{ |chan|
		Trig1.ar(BinaryOpUGen('==', (count + (numChannels - 1 - chan) + 1) % numChannels, 0), SampleDur.ir);
	};
};

var multiChannelPhase = { |triggers, windowRate|
	triggers.collect{ |localTrig|
		var hasTriggered = PulseCount.ar(localTrig) > 0;
		var localPhase = Sweep.ar(localTrig, windowRate * hasTriggered);
		localPhase * (localPhase < 1);
	};
};

{
	var numChannels = 4;
	var tFreq = \tFreq.kr(400);
	var trig = Impulse.ar(tFreq);
	var triggers = multiChannelTrigger.(numChannels, trig);
	var windowRate = tFreq / min(\overlap.kr(1), numChannels);
	var phase = multiChannelPhase.(triggers, windowRate);
	triggers;
}.plot(0.01)
)

no initial trigger:
grafik

correct distribution of phases across the channels:
grafik

Does someone has an idea?

pulsedivider seems not to work with an initial trigger of Impulse.ar(0). When i use a phasor and start it one sample frame earlier and then creating triggers with rampToTrig it seems to work fine:

(
var multiChannelTrigger = { |numChannels, trig|
	numChannels.collect{ |chan|
		PulseDivider.ar(trig, numChannels, numChannels - 1 - chan);
	};
};

var rampToTrig = { |phase|
	var history = Delay1.ar(phase);
	var delta = (phase - history);
	var sum = (phase + history);
	var trig = (delta / sum).abs > 0.5;
	Trig1.ar(trig, SampleDur.ir);
};

{
	var numChannels = 4;
	var rate = 400;
	var phase = (Phasor.ar(DC.ar(0), rate * SampleDur.ir) - SampleDur.ir).wrap(0, 1);
	var trig = rampToTrig.(phase);
	multiChannelTrigger.(numChannels, trig);
}.plot(0.01)
)

grafik

This may be a bug in PulseDivider’s initialization. There’s already an open issue for UGen init bugs related to Impulse, but I forget the number.

hjh

thanks for clarifying. I guess this i then also true for Trig1 which i have tried to use instead of Pulsedivider because of this issue.

The Phasor / rampToTrig approach is nice, however tricky when beeing used together with Demand Ugens, which need the initial trigger. If you then additionaly use the slope of the phasor with Delay1 and create a trigger from it, once more delayed one sample. You have three different unsynced triggers Impulse.ar(0), rampToTrig half a sample later and the trigger from the slope one sample later. This is currently messing up my multichannel sequencer.

I cannot observe a problem with PulseDivider and initial triggers (SC 3.12.1):

play {
	var tr0 = Impulse.ar(0);
	PulseDivider.ar(tr0, div: 3, start: 2).poll(tr0);
}
// -> UGen(PulseDivider): 1

Works for me:

(
var multiChannelTrigger = { |numChannels, trig|
	PulseDivider.ar(trig, div: numChannels, start: (0..numChannels-1).reverse);
};

{
	var numChannels = 4;
	var tFreq = \tFreq.kr(400);
	var trig = Impulse.ar(tFreq);
	var triggers = multiChannelTrigger.(numChannels, trig);
	triggers;
}.plot(0.01)
)

thanks for sharing, but with your example i still dont get the first trigger:

grafik

@dietcv

Hello! I have an oddity here.

The code you posted in the first post of this thread works with SuperCollider 3.12.0 on Raspberry OS and produces the following result:

Screenshot 2024-01-28 at 1.17.47

However, in

  • SuperCollider 3.13.0 on macOS 13.6.4 and
  • SuperCollider 3.14.0_dev on macOS 13.6.4, Ubuntu 22.04 and Windows,

the same code returns the following error:

ERROR: Message 'addKr' not understood.
RECEIVER:
   nil
ARGS:
   Symbol 'tFreq'
   Integer 400

PROTECTED CALL STACK:
	Meta_MethodError:new	0x14071f240
		arg this = DoesNotUnderstandError
		arg what = nil
		arg receiver = nil
	Meta_DoesNotUnderstandError:new	0x140721580
		arg this = DoesNotUnderstandError
		arg receiver = nil
		arg selector = addKr
		arg args = [ tFreq, 400 ]
	Object:doesNotUnderstand	0x140543300
		arg this = nil
		arg selector = addKr
		arg args = nil
	NamedControl:init	0x1600bb940
		arg this = a NamedControl
		var prefix = nil
		var str = tFreq
	Meta_NamedControl:new	0x1600bab00
		arg this = NamedControl
		arg name = tFreq
		arg values = [ 400 ]
		arg rate = control
		arg lags = nil
		arg fixedLag = false
		arg spec = nil
		var res = nil
	a FunctionDef	0x1504254a8
		sourceCode = "<an open Function>"
		var numChannels = 4
		var tFreq = nil
		var trig = nil
		var triggers = nil
		var windowRate = nil
		var phase = nil
	a FunctionDef	0x140ba3a00
		sourceCode = "<an open Function>"
	a FunctionDef	0x1409f9b40
		sourceCode = "<an open Function>"
	a FunctionDef	0x1601f5e00
		sourceCode = "<an open Function>"
	Function:prTry	0x1409fcf40
		arg this = a Function
		var result = nil
		var thread = a Thread
		var next = nil
		var wasInProtectedFunc = false
	
CALL STACK:
	DoesNotUnderstandError:reportError
		arg this = <instance of DoesNotUnderstandError>
	< closed FunctionDef >
		arg error = <instance of DoesNotUnderstandError>
	Integer:forBy
		arg this = 0
		arg endval = 0
		arg stepval = 2
		arg function = <instance of Function>
		var i = 0
		var j = 0
	SequenceableCollection:pairsDo
		arg this = [*2]
		arg function = <instance of Function>
	Scheduler:seconds_
		arg this = <instance of Scheduler>
		arg newSeconds = 19.533960583
	Meta_AppClock:tick
		arg this = <instance of Meta_AppClock>
		var saveClock = <instance of Meta_SystemClock>
	Process:tick
		arg this = <instance of Main>
^^ ERROR: Message 'addKr' not understood.
RECEIVER: nil

must be a new bug then, no issues with mine (self-compiled):

$ scsynth -v
scsynth 3.12.1 (Built from branch 'main' [b6787134d])
1 Like

SC 3.12.0 on Windows (intel) gives the following result when evaluating the code in the first post in this thread:

Screenshot 2024-01-30 at 8.54.53 AM

SC 3.12.2 on MacOS (silicon) gives the following result when running the code in the first post in this thread:

Only SC3.12.0 on Raspberry Pi seems to show the first pulse: the thick vertical line.

Plotter’ in SC3.12.x gives a different result, at least for this code, depending on the OS.
Plotter’ in SC3.13.0 and above results in the following error:

ERROR: Message 'addKr' not understood.

Yes, I can see at a quick glance that PulseDivider doesn’t reset it’s initial state, so it’ll miss an initial trigger

void PulseDivider_Ctor(PulseDivider* unit) {
    SETCALC(PulseDivider_next);

    unit->m_prevtrig = 0.f;
    unit->mLevel = 0.f;
    unit->mCounter = (long)floor(ZIN0(2) + 0.5);

    PulseDivider_next(unit, 1);
}

Depending on which version of SC you’re running, you may or may not have various fixes to UGen initializations. Impulse was fixed in 3.13.

A fix for Trig1 is waiting to be merged.

And what’s more, BinaryOpUGen (which you’re using for ==) also doesn’t reset it’s state (prevA/B), so it may be fooling Trig1 anyway.

void BinaryOpUGen_Ctor(BinaryOpUGen* unit) {
    unit->mPrevA = ZIN0(0);
    unit->mPrevB = ZIN0(1);
    bool initialized = ChooseOperatorFunc(unit);
    if (unit->mCalcRate == calc_DemandRate) {
        OUT0(0) = 0.f;
    } else {
        if (!initialized)
            (unit->mCalcFunc)(unit, 1);
    }
}

So you may be working at the intersection of like 3 different init sample bugs :grimacing:

Actually BinaryOpUGen can’t know it’s previous state (without making assumptions), so it won’t ever work as an initial trigger (important caveat!).

If you don’t mind a 1-sample delay in your system, this works (3.13, 3.12.2):

(
	var multiChannelTrigger = { |numChannels, trig|
		var count = Demand.ar(trig, 0, Dseries(0, 1, inf));
		numChannels.collect{ |chan|
			Trig1.ar(
				TDelay.ar( // <<<<<<<<<<<< ADDED
					BinaryOpUGen('==', (count + (numChannels - 1 - chan) + 1) % numChannels, 0),
					SampleDur.ir),
				SampleDur.ir);
		};
	};

	var multiChannelPhase = { |triggers, windowRate|
		triggers.collect{ |localTrig|
			var hasTriggered = PulseCount.ar(localTrig) > 0;
			var localPhase = Sweep.ar(localTrig, windowRate * hasTriggered);
			localPhase * (localPhase < 1);
		};
	};

	{
		var numChannels = 4;
		var tFreq = \tFreq.kr(400);
		// var trig = TDelay.ar(Impulse.ar(tFreq), 1/s.sampleRate);
		var trig = Impulse.ar(tFreq);
		var triggers = multiChannelTrigger.(numChannels, trig);

		var windowRate = tFreq / min(\overlap.kr(1), numChannels);
		var phase = multiChannelPhase.(triggers, windowRate);
		triggers;
	}.plot(0.01)
)

Note, you might normally more simply use Delay1 instead of TDelay with a 1-sample delay (although TDelay is likely slightly more efficient), but you would need unmerged fixes to Delay1.

My suggestion on this, years ago, was that the only places in the UGens codebase that know whether the incoming data are to be used as a trigger are the trigger inputs themselves – therefore the UGen receiving the trigger is responsible for initializing mPrevTrig to 0 and nowhere else in the code should be responsible for this.

If trigger-receiving UGens actually did this consistently, then BinaryOpUGen would work as a trigger.

The problem with PulseDivider is that the Ctor can leave mPrevTrig nonzero, when the only valid value for mPrevTrig at the end of the Ctor is 0.

hjh

Sorry I misspoke about binop as a trigger. I got turned around thinking it was reading in the trigger, but it is generating it. And prevA/B are used for k-to-a interpolation, not triggering. So it works as a trigger in the original example.

The problem is with the initialization of Trig1. I tested it on 3.14-dev with the pending fix to Trig1 I linked to above and it works as @dietcv originally posted.

In any case the workaround I posted should still work in the meantime (assuming 1-sample delay is OK).

thanks!

so the initialisation issue with:

  • Trig1 will be fixed with SC 3.14
  • Delay1 is waiting to be merged for a future release
  • PulseDivider remains unfixed

is this correct?

Trig1 and Delay1/2 should both be in 3.14, PulseDivider is part of TriggerUGens, which isn’t currently in scope of the init sample fix “project”, so that one is TBD…

1 Like

Also, it might not be a bad idea to mention the following problem:

1 Like