I think my confusion partly stems from the fact that you don’t divide the result of operatorPM
by 2pi
in your original example of phase modulation with feedback. So I was wondering if there was a reason for that, or if it was a mistake?
I learned how to incorporate UGens in Python functions (as opposed to exclusively using them in a SynthDef) in Supriya. So I was able to refactor my code to more closely reflect your sclang example. I’m dividing the result of phase_modulation_operator
by TWO_PI
in both cases. Here it is:
def one_pole_filter(
frequency: UGenRecursiveInput,
modulator: UGenRecursiveInput,
) -> UGenOperable:
nyquist = SampleRate.ir() / 2
clipped_frequency = frequency.clip(-nyquist, nyquist)
slope = abs(clipped_frequency) * SampleDur.ir()
return OnePole.ar(source=modulator, coefficient=(-TWO_PI * slope).exponential())
def phase_modulation_operator(
frequency: UGenRecursiveInput,
modulator: UGenRecursiveInput=0,
) -> UGenOperable:
phase = Phasor.ar(trigger=0, rate=frequency * SampleDur.ir())
return SinOsc.ar(frequency=DC.ar(source=0), phase=(phase + modulator) * TWO_PI)
@synthdef()
def phase_modulation(
amplitude=0.2,
carrier_adsr=(0.01, 0.3, 0.5, 3.0),
carrier_curve=(-4),
carrier_frequency=440,
carrier_phase_index=1.0,
carrier_ratio=1,
gate=1,
modulator_adsr=(0.01, 0.3, 0.5, 3.0),
modulator_curve=(-4),
modulator_ratio=1,
modulator_phase_index=1.0,
) -> None:
carrier_envelope = EnvGen.kr(
envelope=Envelope.adsr(
attack_time=carrier_adsr[0],
decay_time=carrier_adsr[1],
sustain=carrier_adsr[2],
release_time=carrier_adsr[3],
curve=carrier_curve[0],
),
done_action=2,
gate=gate,
)
modulator_envelope = EnvGen.kr(
envelope=Envelope.adsr(
attack_time=modulator_adsr[0],
decay_time=modulator_adsr[1],
sustain=modulator_adsr[2],
release_time=modulator_adsr[3],
curve=modulator_curve[0],
),
done_action=2,
gate=gate,
)
ratio = IRand.ir(minimum=1, maximum=3)
carrier_ratio = ratio
modulator_ratio = ratio * 2
modulator_frequency = carrier_frequency * modulator_ratio
carrier_frequency *= carrier_ratio
modulator = phase_modulation_operator(frequency=modulator_frequency) / (TWO_PI * modulator_phase_index) * modulator_envelope
modulator = one_pole_filter(frequency=modulator_frequency, modulator=modulator)
carrier = phase_modulation_operator(frequency=modulator_frequency, modulator=modulator) / (TWO_PI * carrier_phase_index) * carrier_envelope
pan = Pan2.ar(source=carrier, position=0.0, level=amplitude)
Out.ar(bus=0, source=pan)
@synthdef()
def feedback_phase_modulation(
adsr=(0.01, 0.3, 0.5, 3.0),
amplitude=0.2,
curve=(-4),
frequency=440,
feedback_index=1.0,
gate=1,
) -> None:
ratio = IRand.ir(minimum=1, maximum=3)
carrier_ratio = ratio
envelope = EnvGen.kr(
envelope=Envelope.adsr(
attack_time=adsr[0],
decay_time=adsr[1],
sustain=adsr[2],
release_time=adsr[3],
curve=curve[0],
),
done_action=2,
gate=gate,
)
feedback = LocalIn.ar(channel_count=1) / TWO_PI * feedback_index
modulator = one_pole_filter(frequency=frequency, modulator=feedback)
frequency *= carrier_ratio
carrier = phase_modulation_operator(frequency=frequency, modulator=modulator) / TWO_PI
LocalOut.ar(source=carrier)
carrier *= envelope
pan = Pan2.ar(source=carrier, position=0.0, level=amplitude)
Out.ar(bus=0, source=pan)