What is the intended behaviour of wrapping?

Hello all

I realise that I have either stumbled on a bug or a hole in my understanding of how wrapping works in SuperCollider (probably the latter).

Slightly related: There is a pretty nasty bug right now where if you input a .kr ugen into a .ar Wrap for example it will lead to nan and other nastyness. (see https://github.com/supercollider/supercollider/issues/4261).

Anyways, a simple example that has confused me quite a lot and lead to this post:

Using .wrap to wrap a number between -1.0 and 1.0 works as I would expect, until the receiver reaches 1.0. When the receiver is 1.0 it is wrapped to -1.0. I would expect this to stay at 1.0 but then when the receiver exceeds that number it would wrap back to the minus realm.

// These two are equivalent - Why?
-1.0.wrap2(1) == 1.0.wrap2(1)

Another example. Let’s make a list of numbers from -2.0 to 2.0.

(-2.0,-1.9 .. 2.0).wrap2(1).plot;

Leads to this plot. It never reaches 1.0 - and this confuses me profoundly.

1 Like

Consider a range 0.0 to 1.0.

What would you expect 1.01 to wrap to? 0.01, right?

How about 1.000001? 0.000001.

This could continue indefinitely, so you could say that the limit, as x approaches infinity, of 1 + (10 ** -x) wrapped to 0.0 … 1.0 is 0.0.

So then what is the dividing line, where this behavior of wrapping a value just above 1 down to a value just above 0 changes to wrapping 1.0 to 1.0?

You could say then that 1.0 could wrap to 0.0 (based on the limit) or to 1.0 if you assume an inclusive upper bound. Which is nonsense – it’s necessary to pick one.

So the decision was to wrap to a range a <= x < b – non-inclusive at the top.

hjh

3 Likes

Thanks a lot for the answer. So the thing with .wrap then is that it is non inclusive of the upper bound, if I understand you correctly?

How does one wrap a number then inclusive of the upper bound in SC? eg a <= x <= b ?

To answer my own question, this is a way to achieve what I imagine here:

(
Ndef(\wrappp, {|inNum=0, wrapmin=(-1.0), wrapmax=1|

var hasReachedMax = BinaryOpUGen('==', inNum, wrapmax);

Select.kr(hasReachedMax.poll(label: \maxReached),
[
	Wrap.kr(inNum,wrapmin,wrapmax),
	wrapmax
]).poll(label: \result)

})
)

Ndef(\wrappp).set(\inNum, 1.0) // 1.0
Ndef(\wrappp).set(\inNum, (-1.0)) // 1.0
Ndef(\wrappp).set(\inNum, (1.5)) // -0.5

To give another perspective: .wrap is basically floating point modulo with possible offset and well-defined handling of negative input. Modulo generally computes the remainder of a division. Modulo 1.0 is a special case: it gives the fractional part of a floating point number. Obviously, this must be smaller than 1.0.

1 Like

But your result is non inclusive of the lower bound now: a < x <= b, and not a <= x <= b.

If it’s truly a <= x <= b, then you have either an impossible or an undesirable situation: either x at a boundary must wrap to a and b simultaneously (impossible), or x + b - a would wrap to a different number from x (which is arguably less intuitive than the current behavior).

That isn’t a mathematically formal reductio ad absurdum but it’s pointing in that direction.

hjh

Sorry, that was a typo from my part. My bad. It does actually wrap between -1.0 and 1.0 - at least from my tests. Sorry for being thick headed about this… It’s hard for me to wrap (no pun intended but it’s a good one, so why not…) my head around this.

Anyway, the primary use case for me in this scenario is modulating the position of Pan2 (and similar) with Ugens that exceed the boundaries of -1.0 to 1.0. But Instead of hard clipping I want the signal to be panned from -1.0 to 1.0 and then back from -1.0 when the position value exceeds 1.0. If that makes sense (especially in circular multi channel setups)

1 Like

Let’s say you want fully inclusive wrapping. For simplicity let’s set a to 0.0 and b to 1.0.

Let’s also assume a fixed-point decimal system with one digit below the decimal point: 0.0, 0.1, 0.2 etc. Other numbers don’t exist.

Let’s also consider only positive numbers for now.

So 0.0 wraps to 0.0 (inclusive lower bound), 0.1 to 0.1 … and we want the upper bound to be inclusive, so 1.0 wraps to 1.0 (and therefore cannot wrap to 0.0).

What, then, is the smallest (positive) number that could wrap to 0?

That’s 1.1 → 0, then 1.2 → 1.1, … up to 2.1 → 1.0.

Then 2.2 → 0 and so on.

So 1.0 wraps to 1.0, 2.0 wraps to 0.9, 3.0 wraps to 0.8. Not sure we wanted that.

If we say 2 decimal places, then 1.01 → 0, 2.02 → 0 and so on.

If we let k be the number of places below the decimal point, and c = 10 ^ (-k), then:

  • The smallest positive number that can wrap to 0 is 1 + c. (More generally, the smallest number above b that can wrap to a is b+c.)
  • That’s also the true wrapping interval – the sequence repeats at intervals of 1+c. (General: the periodicity is b-a+c.)

At higher values of k (more decimal places), the behavior is closer to the b-a range but still inexact: 1+c could be 1.0000000000000001.

So then consider infinite precision: k approaches infinity. Then c approaches 0. At this point, 2.0 would wrap to 0, and 3.0 would wrap to 0 – nice clean intuitive behavior there – but 1.0 also wraps to 0 (because the “smallest number above b that wraps to a” is b+c, but c has approached 0, so b itself must wrap to a). This makes the upper bound exclusive, no longer inclusive.

So you can’t have both. If you want both bounds to be inclusive, you have to choose some value > b to be the “smallest value > a to wrap back to a” and accept that the periodicity will deviate from b-a: the further away from a and b you go, the more the result will be skewed. (At high precision, or if you’re not wrapping from far away, this might be acceptable.) If you can’t accept a compromise on periodicity, then one or the other bound must be exclusive.

You’ve handled only one special case. What happens between 20.0 and 22.0? I bet you’ll see an exclusive bound here.

hjh

1 Like

I want the signal to be panned from -1.0 to 1.0 and then back from -1.0 when the position value exceeds 1.0.

I think wrap does what you want?

{ var x = SinOsc.ar(220, 0) * 1.1; [x, x.wrap(-1,1)] }.plot()

Ps. Sc has different rules for integers, which make sense for indexing &etc.

(-1 .. 5).collect({ arg x; x.wrap(0, 4) }) == [ 4, 0, 1, 2, 3, 4, 0 ]
(-1.0 .. 5).collect({ arg x; x.wrap(0, 4) }) == [ 3, 0, 1, 2, 3, 0, 1 ]

No, it doesn’t.

{ Line.ar(0, 1, 0.005).wrap(-1, 1) }.plot;

The last half of this graph is at -1, rather than +1.

The Line reaches 1.0, but the upper bound for Wrap is exclusive. An input value equal to the upper bound will wrap around to the lower bound.

The question was about how to make both upper and lower bounds inclusive.

The answer is to compromise periodicity of the function: { Line.ar(0, 1, 0.005).wrap(-1, 1 + 1e-7) }.plot; does not wrap back down to the bottom. But:

(1.0 .. 10.0).wrap(-1.0, 1.0)
-> [ -1.0, 0.0, -1.0, 0.0, -1.0, 0.0, -1.0, 0.0, -1.0, 0.0 ]

(1.0 .. 10.0).wrap(-1.0, 1+1e-7)
-> [ 1.0, -1.0000000028043e-07, 0.9999999, -2.0000000056086e-07, 0.9999998, -3.0000000084129e-07, 0.9999997, -4.0000000112173e-07, 0.9999996, -5.0000000229034e-07 ]

It is very likely that a deviation of 0.0000001 away from the expected 2.0-unit periodicity would not be audible, especially within the first couple of orders of magnitude.

As discussed above, if it’s important to have zero deviation away from a period = hi - lo, then it’s impossible to have lo <= x <= hi. SC’s default behavior is to prioritize the function’s period.

hjh

1 Like

Yes, but for circular panning +1 is -1? It will work as expected?

I just meant it’s the correct function?

The SunVox manual has a nice diagram of the various wrap/fold functions:

https://warmplace.ru/soft/sunvox/manual.php#distortion

Also, I think you can define wrap to keep both left and right values in if you like, i.e.

var wrap = {
    arg l, r, x;
    var d = r - l;
    (x < l).if({ wrap.value(l, r, x + d) }, { (x > r).if({ wrap.value(l, r, x - d) }, { x }) })
};
(-1, 0 .. 6).collect({ arg x; wrap.value(0, 5, x) }) == [ 4, 0, 1, 2, 3, 4, 5, 1 ]

With floating point there other problems…

0.1 * 2 <= 0.2 == true
0.1 * 3 <= 0.3 == false

I see, I was reading the requirement from the earlier posts too literally.

The key point, then, is that for circular panning, you don’t want both bounds to be inclusive – only one of them – because the period is important, not the upper bound.

So then most of the thread is moot.

hjh

1 Like

How about fold though? It seems to be inclusive of both upper and lower bounds. What’s different about folding vs wrapping at the fuzzy boundary of upper/lower and near upper/lower values?

I’m not sure I understand why this should be the case, shouldn’t it be as Mads originally expected, any value above +1 should wrap around, but +1 should firmly be in one speaker, as -1 is in the other?

Circular panning maps the interval from -1 to 1 to a circle - see PanAz.

When you only have 2 speakers, when you go off the L end you jump to hard R and continue leftward…

In that situation hard left and hard right are the same (at the limit). Think of cutting a circle and opening it up so it makes a line.

It’s off topic here but there is no way to represent the real numbers digitally with finite resources (you can only represent countable sets so the rationals ok, the reals,not) - there will always be compromises when trying to represent continuua!

This analogy makes me understand why one of the bounds is inclusive and the other isn’t… So then fold would be like cutting a circle in half, opening it to make a line, and folding it in half along the other axis, so that one slice is directly on top of the other? Then you’d get both bounds to be inclusive?

The problem with wrap arises because of the discontinuity - you need to decide which end of the image will be closed if you want the value of the function to be everywhere well defined. With fold there’s no discontinuity so no issues.