Implement zerox~ in SC

What the SuperCollider closest implementation of zerox~ Max/MSP object might look like? What would be the difference between my code below and actual zerox~?

How to convert ZeroCrossings output to a number of zero-crossings per signal vector?

(
{
	ZeroCrossing.ar(
		LFDNoise1.ar(MouseY.kr(0.1, 1000))
	).poll;
	nil;
}.scope;
)

Should output clicks at zero-crossings:

(
{ 
	Changed.ar(
		ZeroCrossing.ar(
			LFDNoise1.ar(MouseY.kr(0.1, 1000))
		)
	);
}.scope;
)

Would be grateful for any comments, especially about vector size thing.

Not sure if these are UGens, but they don’t need to be. Trig will give you a trigger on each cross from negative to non-negative, so if you put the signal and its inverse through Trigs, you can get all the zero crossings:

//trig on zero crossings
(
{
    var a;
    a = SinOsc.ar(SinOsc.kr(1, 0, 20,30), 0, 0.1);
	b = Trig1.ar(a, 1/s.sampleRate)+Trig1.ar(a.neg, 1/s.sampleRate);
    [a, b]
}.play;
)

//count the pulses
(
{
    var a;
    a = SinOsc.ar(SinOsc.kr(1, 0, 20,30), 0, 0.1);
	b = Trig1.ar(a, 1/s.sampleRate)+Trig1.ar(a.neg, 1/s.sampleRate);
	i = Impulse.ar(1);
	c = PulseCount.ar(b, Delay1.ar(i)); //delay the trigger by one sample so that polls the sum before it resets
	Poll.ar(i, c);
	nil
}.play;
)
1 Like

Your examples work, thank you @Sam_Pluta. Never used Trig1 before.

Please, can you explain why these two examples sound different?

(
{
	var src, sig;
	src = SinOsc.ar(SinOsc.kr(1, 0, 20,30), 0, 0.1);
	sig = Trig1.ar(src, 1/s.sampleRate)+Trig1.ar(src.neg, 1/s.sampleRate);
    sig;
}.play;
)

(
{
	Changed.ar(
		ZeroCrossing.ar(
			SinOsc.ar(SinOsc.kr(1, 0, 20,30), 0, 0.1)
		)
	);
}.scope;
)

From the help docs…

Trig1

When a nonpositive to positive transition occurs at the input, Trig1 outputs 1 for the specified duration, otherwise outputs 0.

ZeroCrossing

Outputs a frequency based upon the distance between interceptions of the X axis. The X intercepts are determined via linear interpolation so this gives better than just integer wavelength resolution. This is a very crude pitch follower, but can be useful in some situations.

Hopefully that clears it.
ZeroCrossing might not change regularly, in fact, as its designed as a pitch tracker (albeit, a crude one) it should update only when it thinks the frequency has changed - if the signal is periodic in the short term, it shouldn’t change too often. In short, ZeroCrossing is not the tool to use if you want a signal when an input crosses zero - admittedly, it has quite a poor name.
Trig1 updates when ever the value crosses from a negative number to a positive… what isn’t necessarily obvious is it will tread -0 and +0 differently. Nevertheless, Trig1 is the correct tool.

sign

However, an alternative would be to use sign.

(
{
	var src = SinOsc.ar(SinOsc.kr(1, 0, 20,30), 0, 0.1);
	Changed.ar(src.sign);
}.play;
)

sign return -1 if negative, and 1 if positive.

The issue with Trig is the fragility of s.sampleRate.reciprocal and the two sides of the plus … sample rate might change if you use multiple servers, or you might type it wrong, there could be some odd rounding error, or you could copy the two expressions either side of the plus wrong.

1 Like

Thanks @jordan for the detailed answer despite the obvious information contained in the documentation.

Sorry if that came off as condensing - certainly didn’t mean to be -, it’s just the reason why it’s different is explained there. If you put in a sine wave with a steady frequency, it should change once, and then never again as zero crossing outputs a frequency calculated by using the zero crossing.

Just wanted to say that everything is clear now. Also I like the use of sign method here.

The definition of a trigger is a transition from non-positive to positive. If the Trig1 output occurs one sample after this transition, then it’s violating the definition and that should be fixed.

But a/ it isn’t breaking the definition and b/ that isn’t the cause of a “one-sample delay” anyway. (There isn’t a delay; let’s not start folklore going here.)

(
// signal divides nicely into the sample rate
p = {
	var sig = SinOsc.ar(SampleRate.ir/10);
	var trig = Trig1.ar(sig, 1/s.sampleRate);
	[sig, trig]
}.plot(20/s.sampleRate)
)
// hit 'm' to see sample by sample values

p.value[0].size
p.value[0][8..12]

-> [ -0.95105642080307, -0.58778524398804, -2.3399479687214e-08, 0.58778518438339, 0.95105642080307 ]

Sample 10 is not a zero crossing! Sample 9 is negative and so is sample 10.

It’s risky to read a graph and tell yourself “the trigger should be happening there” because graphs don’t have enough precision (even with 1920 vertical pixels, you can’t see a difference on the order of 10e-08).

hjh

Ok, I see I got wrong. I will remove my post not to leave a confusing and wrong answer blowing in the wind.