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
TriOS from OversamplingOscillators has an audio rate phase modulator:
Sam
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.