You could write a function for a single pm operator, the onpole filter and an operator stack.
The Yamaha DX7 Patent uses a different kind of averaging filter, but the tracking OnePole filter is even better.
The index of your modulator is naturally scaled to radians, so you want to make sure to divide it by 2pi. If you are dealing with SinOscs only, you could skip that step and just add the modulation after multiplying the phase by 2pi. But if you want to exchange your carrier for a Wavetable Osc, you have to first divide by 2pi to normalize the modulation and afterwards multiply by BufFrames otherwise your index is not correctly scaled.
Additionally you could write a function for the self modulation operator.
You also want to make sure that your Oscillators have a phase reset. This doesnt matter if you use event based scheduling with Pbind or Routines, but it does matter if you use Pmono or a different method of event scheduling.
here are two parallel stacks:
(
var onepoleLP = { |sig, freq|
var nyquist = SampleRate.ir / 2;
var freqClipped = freq.clip(nyquist.neg, nyquist);
var slope = freqClipped.abs * SampleDur.ir;
OnePole.ar(sig, exp(-2pi * slope));
};
var operatorPM = { |reset, freq, mod = 0|
var phase = Phasor.ar(reset, freq * SampleDur.ir);
SinOsc.ar(DC.ar(0), phase + mod * 2pi);
};
var operatorStack = { |reset, carrFreq, modRatio, pmIndex|
var modFreq = carrFreq * modRatio;
var modulator = operatorPM.(reset, modFreq) / 2pi * pmIndex;
modulator = onepoleLP.(modulator, modFreq);
operatorPM.(reset, carrFreq, modulator);
};
{
var reset;
var parallel, stack_A, stack_B;
reset = \reset.tr(0);
stack_A = operatorStack.(reset, \carrFreq_A.kr(100), \modRatio_A.kr(1.5), \pmIndex_A.kr(2.5));
stack_B = operatorStack.(reset, \carrFreq_B.kr(200), \modRatio_B.kr(2.0), \pmIndex_B.kr(1.0));
parallel = [stack_A, stack_B].sum;
}.plot(0.021);
)
here is an operator with feedback:
s.options.blockSize = 1;
(
var onepoleLP = { |sig, freq|
var nyquist = SampleRate.ir / 2;
var freqClipped = freq.clip(nyquist.neg, nyquist);
var slope = freqClipped.abs * SampleDur.ir;
OnePole.ar(sig, exp(-2pi * slope));
};
var operatorPM = { |reset, freq, mod = 0|
var phase = Phasor.ar(reset, freq * SampleDur.ir);
SinOsc.ar(DC.ar(0), phase + mod * 2pi);
};
var operatorFb = { |reset, freq, fbIndex|
var fbIn, sig, mod;
fbIn = LocalIn.ar(1);
mod = fbIn * fbIndex;
mod = onepoleLP.(mod, freq);
sig = operatorPM.(reset, freq, mod);
LocalOut.ar(sig);
sig;
};
{
var reset = \reset.tr(0);
operatorFb.(reset, \carrFreq_A.kr(100), \fbIndex.kr(1));
}.plot(0.021);
)
for self modulation you expect the SinOsc to get a Saw shape (make sure to boot with blocksize = 1)