Modulating phase offset of a triangle shape

I am using LFTri in a tremolo effect and I would like to be able to modulate the phase offset of one side of a 2 channel signal, which is not possible with LFTri. Are there any other Ugens I can use for this? It does not have to be a perfect triangle shape, approximating a triangle shape with ie. SinOsc would also work for me.

One thing I learned from Pure Data land (where thereās no built-in triangle oscillator) is that you can make a triangle out of a phasor by subtracting half the phasorās amplitude (so that itās centered around 0) and then taking the absolute value. (Hm, if LFSaw is already centered around 0, then just .abs would get you the triangle shape.) This triangle is DC-offset positively, so youād have to subtract again and scale it to the range you want, but it does get you the shape.

I think you should be able to add any modulator to the phasor, and modulo that back into the pre-`.abs` range, and this would be phase modulation of the triangle wave.

hjh

The phasor trick works as recreation of LFTri. There is quite possibly a more elegant way to go about but this seems to work

``````(
{
var rate = 100;
var phase = 4.0.rand;
var p = 1 - Phasor.ar(0, rate / s.sampleRate * 4, -2 + phase, 2 + phase).fold(-1, 1) - 1;
[ p, LFTri.ar(rate, phase) ]
}.plot
)

``````

In the example below, the phasor-to-triangle approach works well when staying at a specific value, but not so great when you sweep the phase in the gui - compare to using SinOsc as the lfo. Sweeping the phase using SinOsc as the lfo sounds sweet to me but I prefer the characteristics of a triangle shape. I donāt really understand how it is possible to modulate the phase of the SinOsc lfo without creating discontinuities, but nice that it is. I wonder if it is possible to create a smoother modulation with the triangle shape by way of phasor OR if I can distort a sinusoidal shape to approximate a triangle?

``````(
Ndef(\test, {
var rate = 1;
var phase = [ DC.kr(0), \phase.kr(0) ];
var trig = Changed.kr(phase);
var lfo = 1 - Phasor.ar(trig, rate / s.sampleRate * 4, -2 + phase, 2 + phase).fold(-1, 1) - 1;
// var lfo = SinOsc.ar(1, phase);
var sig = LFSaw.ar([110, 220]) * 0.1;
Out.ar(0, sig * lfo)
}).gui
)

``````

One difference is that your triangle formula here is resetting the triangleās phase every time the phase offset changes. SinOsc is definitely not doing that. `Changed` and Phasor `trig` is not what you want.

The idea is to add an offset to the phasor before converting to the triangle. I think youāre trying to do that by adding to the Phasorās range inputs, but IMO it would be more direct to just `+` the Phasor signal.

Iām not in a position to write up a full example right now; will try later.

hjh

Iām starting with the idea that a phase-driven oscillator is a wave shaper. This isnāt obvious at first, but: a wave shaper uses an input signal as a lookup `x` into a transfer function. A phasor happens to be an identity operand for wave shaping: just like `1 * x == x`, `phasor waveShape: xfer == xfer` (with frequency scaling according to the phasorās rate).

We would expect, then, `SinOsc.ar(freq)` to be the same as `SinOsc.ar(0, phase)` if the phase input is a phasor running at freq Hz. To verify that, Iāll use LFSaw as the phasor because itās easier to understand the frequency (rate) input ā you can see in the plot that itās a series of linear ramps, matching the definition of a phasor. (Full disclosure: LFSaw seems to run slightly ahead of SinOscās internal phase, but the two SinOsc plots look āclose enough for government work.ā)

``````(
{
var rate = 400;
var sig;

// the phasor
var phase = LFSaw.ar(rate);

// phase modulation will go in here

// the transfer function
// mod isn't strictly necessary yet
// but with phase modulation,
// you can't be sure it will stay in range
sig = SinOsc.ar(0, (phase * pi) % 2pi);

[phase, sig, SinOsc.ar(rate)]
}.plot;
)
``````

Phase modulation, then, is just a matter of modifying the phase before passing it to the transfer function. Your ideal behavior, based on `SinOsc.ar(rate, phasemod)`, adds the phase offset to the internal phasor: simple `+` in our taken-apart SinOsc. (Pay attention, though, to scaling. The LFSaw phasor is -1 to +1; SinOscās phase is 0 to 2pi, or -pi to pi. To get the same effect in SinOsc as phasePlusMinus1 + phase, itās necessary to scale Ā±1 to Ā±pi.)

``````(
{
var rate = 400;
var sig;

// the phasor
var phase = LFSaw.ar(rate);

// phase modulation
var mod = LFDNoise3.ar(500);

phase = phase + mod;

// the transfer function
// DO NOT CHANGE THE TRANSFER FUNCTION
// we are only modulating phase, not messing with anything else
sig = SinOsc.ar(0, (phase * pi) % 2pi);

[phase, sig, SinOsc.ar(rate, mod * pi)]
}.plot;
)
``````

Having tested a working method, then the last step is to substitute a triangle-wave transfer function. First, test against LFTri. (Your use of `fold` is nice, easier than offset-then-abs.)

``````(
{
var rate = 400;
var sig;

var phase = LFSaw.ar(rate);

sig = (phase * 2).fold(-1, 1);

[phase, sig, LFTri.ar(rate)]
}.plot;
)
``````

And apply phase modulation exactly as before.

``````(
{
var rate = 400;
var sig;

var phase = LFSaw.ar(rate);

// phase modulation
var mod = LFDNoise3.ar(500);

phase = phase + mod;

sig = (phase * 2).fold(-1, 1);

// deleted LFTri b/c it's not modulatable
[phase, sig]
}.plot;
)
``````

PS If this doesnāt quite satisfy, you can also fill a buffer with a wavetable-ized triangle wave, and use Osc instead of SinOsc. This uses the same base logic as SinOsc, just with a different wave shape.

``````(
var tri = Signal.fill(1024, { |i| (i/1024 * 4).fold(-1, 1) });

b = Buffer.sendCollection(s, tri.asWavetable);
)

(
{
var rate = 400;
var sig;

var phase = LFSaw.ar(rate);

// phase modulation
var mod = LFDNoise3.ar(287);

phase = phase + mod;

sig = Osc.ar(b, 0, (phase * pi) % 2pi);

[phase, sig, (phase * 2).fold(-1, 1)]
}.plot;
)
``````

hjh

1 Like

TriOS from OversamplingOscillators has an audio rate phase modulator:

Sam

2 Likes

Thanks a lot, very elegant solution. Regarding DC offset: Since I am using this lfo to modulate amplitude (normal tremolo effect) my first instinct is to keep all the lfo values non-negative using .fold(0, 1). A phase-offset of [ 0, 0.5] will now produce out-of-phase amp modulation, ie. when left side is at full amplitude, right side is at zero amplitude and vice versa:

``````Ndef(\test, {
var rate = 1;
var phase = LFSaw.ar(rate);
var lfo = (phase + [0, 0.5] * 2).fold(0, 1);
var sig = LFSaw.ar([110, 220]) * 0.1;
Out.ar(0, sig * lfo)
})
``````

Is this approach āDC-offset-safeā ? Mostly I will be using slow rates for ānormalā tremolo fx, but at times rates will venture into 'ring-modulation-territory '.

Great, would love me some oversampling oscs for other projects, but for this project I am trying to stay as SC-vanilla close as possible.