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

OK… got up, had coffee…

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.