Controlling Blip oscillator's numharm parameter with a control signal

This is my first post and I am totally new to Supercollider, so I hope my question makes sense!

In “A gentle Introduction to Supercollider”, there is an example on p.108 that uses a control signal to affect a Blip UGen’s numharm parameter. I don’t understand how can a Pulse control signal that has its frequency and amplitude continuously modulated (by the mouse) change the number of Blip’s harmonics. I thought that numharm would expect a (possibly continuously varying) integer.

A reduced example:

(
{
	Blip.ar(
		freq: [100, 102],
		numharm: Pulse.kr(freq: MouseX.kr(1, 10), mul: MouseY.kr(0, 1)).range(1, 10),
		mul: 0.1
	);
}.play;
)

Hello ! Welcome to SuperCollider :slight_smile: !

Among the amazing things SuperCollider is made of, there is this ‘UGen/SynthDef’ thing.

UGens have no idea what programming is. They do not know what integer are. In fact, they don’t even know what boolean or those kind of tools are. But you can still use boolean logic when designing synths. That’s weird.

Inside a SynthDef, every value is an UGen. They are array of floats, that keep ‘advancing’ during the synth’s life. There’s nothing more than UGens : no boolean, no integer, and in some certain sense no “float”.

So internally, every synth, like Blip or SinOsc, are designed to take those UGens as parameters inputs. To see how one particular synth maps an UGen to an ‘integer’ value, you’d have to read the source code. That’s not the easiest thing to do. I didn’t read Blip, but I think it was adapted so the floating point value is rounded to the nearest integer, but it is still an UGen.

Same goes for if statements within synthdefs : it does not check against ‘true’, it checks against ‘1.00’, or maybe ‘> 0.0’, might depend, which can be confusing at first.

1 Like

I hope someone with better knowledge than me can give you a detailed answer to this question.

Here’s an experiment I tried to see if I could transform a LFPulse to an ‘integer’ :

// Original signal
{ Pulse.kr(2) }.plot(10);
// Signal forced to positive side
{ Pulse.kr(2).abs }.plot(10);
// Smoothed output
{ Pulse.kr(2).abs.lag(1) }.plot(10);
// Even more smoothed output
{ Pulse.kr(2).abs.lag(1).lag(2) }.plot(10);

So that works, I end up with a ‘constant’ value around 0.5 . Sadly, it’s the amplitude of the LFPulse that dictates the value, not its frequency.

On the server there are only arrays of floats and ‘functions’ that mutate arrays of floats. When one of these ‘functions’ expects a whole number if just rounds.

Synthdefs are a little different as they are abstractions over the server. Ultimately, here there are ugens and floats.
Floats are just created on the server as constants. Ugens do two things, tell the server to create one of these ‘functions’ and say how these ugens and constants are connected together.

Pulse.kr changes a float value on the server, then blip reads that value and probably rounds it to a whole number. All (excluding buffer and IO) of these server ‘functions’ just mutate some floating point number(s). Control signals are a single float per block, and audio signals are many floats per block (an array).

1 Like

An alternative is to construct an empirical test, and observe the result.

Here, we could guess that casting float to int is probably either truncation (1.1 → 1, 1.9 → 1) or rounding (1.1 → 1, 1.9 → 2), because nothing else quite makes sense. If it’s truncation, then breakpoints would be at integer boundaries, so a test range would need to span at least one integer bound. If it’s rounding, then breakpoints are at k + 0.5 (k is int). So if you run numharm between, say, 1.0 and 3.0, then it would cross 1.5 and 2.5 (testing rounding) and 2.0 (testing truncation).

{
    var nh = Line.ar(1, 3, 0.1);
    [nh, Blip.ar(440, nh)]
}.plot(0.1);

… and then it becomes clear that the waveform changes when nh crosses from < 2.0 to >= 2.0.

So the float → int behavior is truncation (which is the normal behavior in C for int x = someFloat).

I find it helpful to think in terms of signal ranges.

  • Pulse (in theory) ranges from -1 to +1. (In practice, it stabilizes to about -0.5 to +0.5, but, because it’s a band limited oscillator, it’s subject to Gibbs effect wiggling at the corners, so it won’t be exactly ± 0.5.)
    • How do I know ± 0.5? Again, empirical testing: plot, and look.
  • Frequency modulation of the pulse wave should not affect its amplitude. So you don’t need to worry about that.
  • Amplitude modulation (mul) does affect its amplitude.
    • MouseY’s inputs specify a range 0 to 1.
    • ± 0.5 * 0 = 0 only.
    • ± 0.5 * 0.5 = ± 0.25.
    • etc… so the pulse now has a minimum amplitude 0, and maximum ~0.5.
  • Then, range. This depends on Pulse’s signalRange, which is bipolar (± 1). So that range instruction translates to “scale ± 1 onto 1–10.”
    • ± 1 spans 2 units. 1–10 spans 9 units. x * 4.5 produces ± 4.5 = the desired 9 unit span.
    • This is 5.5 lower than desired, so x * 4.5 + 5.5.
    • range (using linlin) does this internally for you.
    • But the input range is roughly ± 0.5, not ± 1.0. So you’d really get:
[-0.5, 0.5] * 4.5 + 5.5
-> [ 3.25, 7.75 ]

Which, when truncated to integer, means 3 to 7 harmonics.

(Probably Pulse is a typo. For an LFO, LFPulse would be a better choice. But, LFPulse is a unipolar oscillator, so the details of the math would work out differently.)

hjh

3 Likes

@jamshark70 It took me a while to understand everything, and I have a small question:

But the input range is roughly ± 0.5, not ± 1.0. So you’d really get:

Isn’t the input range 0 to 0.5, as you wrote in the amplitude modulation part?

I don’t think so. The Pulse UGen by itself produces roughly ±0.5. mul is implemented as *, with the Pulse as the input to the multiplication operator. So the output range of the multiplier is -0.5 * MouseY to +0.5 * MouseY. MouseY ranges 0 to 1. If MouseY is 0, then the resulting amplitude is 0, and the range is 0 to 0. If MouseY is 1, then the resulting amplitude is roughly 0.5, and the range is about ±0.5.

“Amplitude” and “range” are not the same. Amplitude is always positive. The range is often bipolar. A sinewave’s range is ±1 but its amplitude (based on peak) is 1. You would not infer from this positive amplitude that the entire sinewave is >= 0.

hjh

Ok, thank you for the detailed responses.