Magnitude == and != methods don't seem to be working in Synths

I’m trying to use == in a SynthDef but it throws an error when attempting to compile it.

!= also throws an error. >, <, >=, <= all work fine.

(
SynthDef(\magTest, { |test = 0|
	var less = (1 * (test < 0)) * SinOsc.ar(400);
	var equal = (1 * (test == 0)) * SinOsc.ar(800);
	var greater = (1 * (test > 0)) * SinOsc.ar(1200);
	var sig = (less + equal + greater) * 0.1;
	Out.ar(0, sig)
}).add
)

ERROR: binary operator '*' failed.
RECEIVER:
   false
ARGS:
   Integer 1
   nil

PROTECTED CALL STACK:
	Meta_MethodError:new	0x7fb418fcc800
		arg this = BinaryOpFailureError
		arg what = nil
		arg receiver = false
	Meta_DoesNotUnderstandError:new	0x7fb418fceb40
		arg this = BinaryOpFailureError
		arg receiver = false
		arg selector = *
		arg args = [ 1, nil ]
	Object:performBinaryOpOnSomething	0x7fb3c8233e40
		arg this = false
		arg aSelector = *
		arg thing = 1
		arg adverb = nil
	a FunctionDef	0x7fb41b637bb8
		sourceCode = "{ |test = 0|
	var less = (1 * (test < 0)) * SinOsc.ar(400);
	var equal = (1 * (test == 0)) * SinOsc.ar(800);
	var greater = (1 * (test > 0)) * SinOsc.ar(1200);
	var sig = (less + equal + greater) * 0.1;
	Out.ar(0, sig)
}"
		arg test = an OutputProxy
		var less = a BinaryOpUGen
		var equal = nil
		var greater = nil
		var sig = nil
	SynthDef:buildUgenGraph	0x7fb3f813c380
		arg this = a SynthDef
		arg func = a Function
		arg rates = nil
		arg prependArgs = [  ]
		var result = nil
		var saveControlNames = nil
	a FunctionDef	0x7fb3f813a9c0
		sourceCode = "<an open Function>"
	Function:prTry	0x7fb41941f080
		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 BinaryOpFailureError>
	Nil:handleError
		arg this = nil
		arg error = <instance of BinaryOpFailureError>
	Thread:handleError
		arg this = <instance of Thread>
		arg error = <instance of BinaryOpFailureError>
	Object:throw
		arg this = <instance of BinaryOpFailureError>
	Function:protect
		arg this = <instance of Function>
		arg handler = <instance of Function>
		var result = <instance of BinaryOpFailureError>
	SynthDef:build
		arg this = <instance of SynthDef>
		arg ugenGraphFunc = <instance of Function>
		arg rates = nil
		arg prependArgs = nil
	< closed FunctionDef >  (no arguments or variables)
	Interpreter:interpretPrintCmdLine
		arg this = <instance of Interpreter>
		var res = nil
		var func = <instance of Function>
		var code = "SynthDef(\magTest, { |test =..."
		var doc = nil
		var ideClass = <instance of Meta_ScIDE>
	Process:interpretPrintCmdLine
		arg this = <instance of Main>
^^ ERROR: binary operator '*' failed.
RECEIVER: false

If I comment out the parts relevant to == I can get the SynthDef working, and setting the \test arg works as expected.

(
SynthDef(\magTest, { |test = 0|
	var less = (1 * (test < 0)) * SinOsc.ar(400);
	// var equal = (1 * (test == 0)) * SinOsc.ar(800);
	var greater = (1 * (test > 0)) * SinOsc.ar(1200);
	var sig = (less + /*equal +*/ greater) * 0.1;
	Out.ar(0, sig)
}).add
)
a = Synth(\magTest)
a.set(\test, -1)

As a workaround I tried the following, which lets me compile the SynthDef, but doesn’t actually get == working:

(
SynthDef(\magTest, { |test = 0|
	var less = (1 * (test < 0)) * SinOsc.ar(400);
	var equal = (1 * (test == 0).asInteger) * SinOsc.ar(800);
	var greater = (1 * (test > 0)) * SinOsc.ar(1200);
	var sig = (less + equal + greater) * 0.1;
	Out.ar(0, sig)
}).add
)
a = Synth(\magTest) // should hear 800hz sine

a.set(\test, -1) // evaluating this lets 400hz sine through

I’m on MacOS Big Sur, running 3.12.2, but it also did it on 3.12.1.

Seems like a bug?

I found this: http://supercollider.sourceforge.net/wiki/index.php/If_statements_in_a_SynthDef
(at the bottom of the page, in the Xor section)

‘==’ doesn’t turn into a BinaryOpUGen automatically. We have to create the BinaryOpUGen by hand.

BinaryOpUGen('==', (x > 0) + (y > 0), 1)

Is this still the way?

I can’t tell if this is a way, but I guess they are easier ways:
Eg:

(
SynthDef(\magTest, { |test = 0|
	var less = SinOsc.ar(400);
	var equal = SinOsc.ar(800);
	var greater = SinOsc.ar(1200);
	var sig = Select.ar(test.sign+1,[less, equal, greater]) * 0.1;
	Out.ar(0, sig)
}).add
)
1 Like

another workaround: (test >= 0) * (test <= 0) should be equivalent to (test == 0)

Thank you both. Both of those would have worked. And I found a different work around that I’m currently using.

I should have been more specific with my example… I just threw it together to illustrate the behavior, not because that was what I was trying to do.

The thing I am trying to do is to set the amplitude modulation amount along with phase inversion for the modulator. Where any value, positive or negative other than 0 sets the modulation amount and possible flips the phase. And since the modulator is multiplying the original signal, with 0 for the mod amount, I still want a way for the the original signal to get through.

I ran into the snag intending to do something like:

(
sig = sig * (
	(am
		* (amAmount * (amAmount > 0)) // set mod amount
		* (amAmount * (amAmount < 0)) // set inverted mod amount
	)
	+ (1 * (amAmount == 0)) // no modulation, only dry signal 
)
)

Which isn’t so great to read anyway.

Thinking of how to achieve it, I eventually came to the code below which is more succinct anyway. Probably would have left it as is had I not needed to figure out a different approach, so maybe for the best :slight_smile:

(
sig = sig * (
	(am	* (amAmount * (amAmount.abs > 0))  // set mod amount, pos or neg
	)
	+ (1 * (amAmount.abs <= 0))); // only dry signal
)

I am curious though, why it is that all the other logical operators work, but not == or !=? I looked at the class’s source code and all of the methods seem to work in the same way, more or less.

And BinaryOpUGen('==', ...., ....) doesn’t seem so great to write.

In recent versions you can use the operators |==| and |!=|. Then pretty much all of the clunky workarounds become unnecessary.

hjh

1 Like

that sounds nice. could you probably share your solution with the |==| and |!=| operators? :slight_smile:

You can just take the initial example and substitute the lazy-equals operator:

(
SynthDef(\magTest, { |test = 0|
	var less = (1 * (test < 0)) * SinOsc.ar(400);
	var equal = (1 * (test |==| 0)) * SinOsc.ar(800);
	var greater = (1 * (test > 0)) * SinOsc.ar(1200);
	var sig = (less + equal + greater) * 0.1;
	Out.ar(0, sig)
}).add
)

hjh

Another hidden gem of SC !
I just wonder: why having chosen this unusual syntax of the simpler == ?

Well, I didn’t quite want to get into it, but… Ok.

There are two types of operation in SC: eager and lazy.

1 + 2
-> 3  // eager

SinOsc.kr + 2
-> a BinaryOpUGen  // lazy

Pwhite(1, 10, inf) * 0.5
-> a Pbinop  // lazy

In the eager operation, all operands can be resolved to actual numbers right now – so the operation is performed right now and you get the answer right now.

In lazy operations, one or more operands are not numbers, but rather objects that describe how some numbers will be calculated in the future. You can’t do the math right now because the numbers aren’t available… but you can compose a new object that describes the operation to be performed later.

In SC, there are many places where == must be eager, even when applied to UGens or patterns. For example, one detail in the example SynthDef here is that the 1 * ... operations will not render into the SynthDef. BinaryOpUGen recognizes that 1 * x always equals x, so it can just return x if it’s certain that one operand will always be 1. In this case, you absolutely cannot accept a future equality check: it is correct to optimize if a or b == 1 right now, and it is wrong to optimize if a or b is something that might produce 1 in the future (but might not).

So James McCartney’s decision was to make == and != always eager, while other comparators can be lazy when needed. You could argue that this was a bad decision, but it is what we’ve got. At one point, I tried to change it so that always-eager == would use a different method selector, but I failed. There are too many places where it’s used eagerly.

The next best solution, then, is to add a lazy-equals operator.

The key point is that SC must be able to distinguish between “I need to know if the thing right in front of me equals something else, right now” and “I need a future equality comparison.” A single operator cannot do that. (Although an operator adverb might also work, e.g. a ==.lazy b, though I’d argue the adverb here is more unusual and more disruptive. However, the adverb might also slow down equality checks, I suspect, so it’s probably better not to go that way.)

Edit: There’s a section in the Object help file on Equality and Identity, and |==| and |!=| are documented there (as well as in the Operators help file). Unfortunately it’s unlikely to be discovered accidentally.

hjh

4 Likes

Thank you for the explanation James! I know you’re trying to slow down your posting on here, but this level of detail really helpful, so I appreciate it.

I’ve encountered needing to use |==| in Pif and never really knew why. I found myself trying both version until one worked as expected.

For me, it’s easier to remember that in cases like the example above I need to use |==| if I know the reason why, and now it also makes a whole lot of sense of why it’d be the case in certain scenarios in Pif. So this really helps!

Just one follow up question, because for me the source of confusion came from the Magnitude help file, where == and != are undocumented:
Is there a way to example statement differently, so that an eager == work? I feel like in these cases the operation will always need to be lazy. If so, when or why does it make sense to have == and != methods for Magnitude?

Client side logical comparisons will always default to providing Booleans rather than 0/1 correct?Or is Magnitude the entirely wrong class, and I just happened to stumbled upon that class’s methods and the description happened to make sense coincidentally?

No, never.

The key is, which behavior do you want? In the example at the start of this thread, it only makes sense to do the == at the time of running the synth in the server, not at the time of building the SynthDef in the client. That’s a lazy operation. Lazy and equal operations are not interchangeable. If you want lazy equals, write lazy equals. If you want eager equals, write eager equals. It doesn’t make sense to expect to be able to write eager equals while desiring lazy equals behavior.

It is a bit confusing that only == and != were defined to be eager, while <, >, <= and >= will always be lazy for lazy classes (patterns, UGens, functions). In hindsight, it may have been better if JMc had defined == and != to behave compatibly with the other comparators, and added a separate right-now equality operator. But that isn’t how it went down, and it would break a lot of code to change it now.

Note though that 1 < 2 makes sense (true), but 1 < Object.new doesn’t make sense (error), while 1 == Object.new does make sense (it’s false – objects of incompatible types are naturally not equivalent). So == and != stand apart from the other comparators in that respect.

Well, consider the code definition for Magnitude:==, which you can look up using ctrl-i in the IDE:

	== { arg aMagnitude; ^this.subclassResponsibility(thisMethod) }

This is a big red flag saying Magnitude is useful as a parent of other classes but it doesn’t do anything by itself. “subclassResponsibility” means just what it says – there are expected to be subclasses doing the real work, while this class merely defines behaviors that may be common to multiple subclasses. (So how does it know that 1 == 1? Because 1 is a SimpleNumber, and SimpleNumber implements ==.)

Also, you are not testing a Magnitude for equality. You’re testing a UGen for equality. (SynthDef arguments are UGens. They have to be. The only way that any value can change while a synth is running is if it’s a UGen. You need to be able to .set controls mid-synth – therefore the value will change – therefore it must be a UGen.) UGen.superclasses – Magnitude is not in this list. UGen does not inherit from Magnitude, so Magnitude and UGen are not connected. What you read in Magnitude’s help file isn’t relevant for UGens at all.

UGen does inherit from AbstractFunction, and this is where lazy math operators are defined for Function, Pattern and UGen (and here is where |==| is different from Object’s |==|).

hjh

4 Likes