Clean fm with sinosc phase?

Hello all,

in the following examples just the first produces a clean fm sound, the examples based on sinosc phase introduce artifacts and don’t sound good. why is this?


({
	var carrier=400;
	var index=SinOsc.ar(1)*3;
	var mod=SinOsc.ar(400,0,index,1);
	var fm=carrier*mod;
	var snd1=SinOsc.ar(fm);
	var snd2=SinOsc.ar(0,Sweep.ar(trig:Impulse.ar(fm),rate:fm)*2pi);
	var snd3=SinOsc.ar(0,Phasor.ar(trig:Impulse.ar(fm),rate:2pi*fm/s.sampleRate,start:0,end:2pi));
	Select.ar(MouseX.kr(0,2).round(1).poll,[snd1,snd2,snd3])   //scan through synths w/ mouse
}.play
)

(edited code part)

thanks,
jan

I recommend as a standard PM formula:

var mod = SinOsc.ar(freq * ratio) * index;
var phase = Phasor.ar(0, freq * SampleDur.ir, 0, 1) * 2pi;
var carrier = SinOsc.ar(0, (phase + mod) % 4pi);

Recommended to take advantage of Phasor’s inherent wraparound, instead of trying to sync a trigger.

Also SinOsc and Osc do not behave correctly when phase exceeds ±8pi.

(Phasor help might benefit from an example of “taking apart” an oscillator like this – freq * SampleDur.ir is the simplest way to get Phasor to obey a frequency, but this formulation doesn’t just leap to mind. I think I took a long time to arrive at it myself.)

hjh

2 Likes

Thank you James! Still im wondering what is causing the noise in the other examples i gave… Is it the trigger usage, or some aliasing issue? Ideally they should work just the same…?

It future, could you post neater code? Having everything written all on one lines takes a large amount of time to read and then understand, limiting the number of the responses you will get. Writing code isn’t just about getting the computer to do what you want, it is also about communicating with over people the intentions behind the code, it is a language after all.

Please use multiple variables with good names, each with a short expression after the equals sign.
Try writing it like this…

{
	var carrier2 = ... some SHORT expression;
	var mod2 = ...;
	var carrier1 = ...
	var mod1 = ...
	var fm = ...
	Out.ar(0, fm);
}.play

Also consider using named arguments when there are multiple - I for one don’t know the arguments of Phasor off the top of my head, and why should anyone when the IDE has a popup :slight_smile:
e.g…

SinOsc.ar(freq: 440, phase: 1, mul: 1, add: 0)

hi @jordan,

thanks for pointing out and sorry for the messy code! i got carried away by the problem and lost track of form.
here a bit more polished example:

({
	var carrier=400;
	var index=SinOsc.ar(1)*3;
	var mod=SinOsc.ar(400,0,index,1);
	var fm=carrier*mod;
	var snd1=SinOsc.ar(fm);
	var snd2=SinOsc.ar(0,Sweep.ar(trig:Impulse.ar(fm),rate:fm)*2pi);
	var snd3=SinOsc.ar(0,Phasor.ar(trig:Impulse.ar(fm),rate:2pi*fm/s.sampleRate,start:0,end:2pi));
	Select.ar(MouseX.kr(0,2).round(1).poll,[snd1,snd2,snd3])   //scan through synths w/ mouse
}.play
)

my question is why do snd 2&3 sound so bad compared to 1, and whether it has to do with the use of a trigger and causing aliasing of some sort…?

thanks,
jan

i think you need a continuous phase, you can modulate the rate of Phasor or Sweep but not the trigger rate of Impulse with a bipolar signal.

({
	var carrier=400;
	var index=SinOsc.ar(1)*3;
	var mod=SinOsc.ar(400,0,index,1);
	var fm=carrier*mod;

	var snd1=SinOsc.ar(fm);
	var snd2=SinOsc.ar(0, LFSaw.ar(fm, 1).linlin(-1, 1, 0, 1) * 2pi);
	
	Select.ar(MouseX.kr(0,1).round(1),[snd1,snd2])
}.play
)

thanks for the reply @dietcv!
i tried switching the impulse input with a non modulated carrier. i makes it better, but still theres an edge to the sound, meaning theres also something else at stake with the triggers…

({
	var carrier=400;
	var index=SinOsc.ar(1)*3;
	var mod=SinOsc.ar(400,0,index,1);
	var fm=carrier*mod;
	var snd1=SinOsc.ar(fm);
	var snd2=SinOsc.ar(0,Sweep.ar(trig:Impulse.ar(carrier),rate:fm)*2pi);
	var snd3=SinOsc.ar(0,Phasor.ar(trig:Impulse.ar(carrier),rate:2pi*fm/s.sampleRate,start:0,end:2pi));
	Select.ar(MouseX.kr(0,2).round(1).poll,[snd1,snd2,snd3])   //scan through synths w/ mouse
}.play
)

if you would like to go with the Impulse / Sweep combination which drives the phase you could also consider pulsar synthesis:

(
var hanningWindow = { |phase|
	(1 - (phase * 2pi).cos) / 2 * (phase < 1);
};

{
	
	var index = SinOsc.ar(1) * 2;
	
	var tFreq = \tFreq.kr(400);
	var grainFreq = \freq.kr(400);
	
	var trig = Impulse.ar(tFreq);
	var phase = Sweep.ar(trig, grainFreq * (1 + (SinOsc.ar(grainFreq) * index)));
	
	var snd = sin(phase * 2pi);

	snd = snd * hanningWindow.(phase);
	
	snd = LeakDC.ar(snd);
	
	snd !2 * 0.25;
	
}.play
)

I’d make one more suggestion for legibility – it’s something very very simple, it baffles me why so many users don’t do it:

Hit the space bar sometimes.

A wall of text with only the minimum required spaces between words is (in my opinion) quite unpleasant to try to make sense of.

//Ican'tbreathe,there'snoroom
var snd2=SinOsc.ar(0,Sweep.ar(trig:Impulse.ar(fm),rate:fm)*2pi);

// ah, I can breathe again
var snd2 = SinOsc.ar(0, Sweep.ar(trig: Impulse.ar(fm), rate: fm) * 2pi);

As for the main question, I suspect that the trigger timing is less exact than you think… but I haven’t tested.

Tbh I’m not really interested in why… after years of different approaches to FM and PM I eventually arrived at a formulation that implements it correctly, so for me it’s just, use a correct formulation.

hjh

thats also a really neat way of going at it, thank you @dietcv! never occurred to me to implement the grain window like that!

Hit the space bar sometimes.

another good advice to take!:slight_smile:

Tbh I’m not really interested in why… after years of different approaches to FM and PM I eventually arrived at a formulation that implements it correctly, so for me it’s just, use a correct formulation.

fair enough, guess its wise as a mortal being to know when to stop going down the rabbit hole in sc;)

1 Like

Oh wait, I see the problem with the triggering approach.

The assumption behind the triggering approach is that the phase should always wrap around to 0.

This assumption could be true, if the wavelength is an exact integer number of samples, although floating-point rounding error might make that ideal impossible to achieve.

But for the vast majority of frequencies, the wavelength is not an integer number of samples. In that case, the correct way for phase to wrap around is modulo, not a hard-sync to 0.

Phasor, left to its own devices, does this in fact. Let’s get the phase samples from a plot, and then split them into subarrays at the wraparound points (so that each subarray is a ramp up).

p = {
	var phase = Phasor.ar(0, 1002 * SampleDur.ir, 0, 1);
	[phase, SinOsc.ar(0, phase * 2pi)]
}.plot;

q = p.value[0];
r = q.separate { |a, b| b < a };

// these are all the reset points
r.collect(_.first);
-> [ 0.0, 0.022448940202594, 0.02217679284513, 0.021904645487666, 0.021632498130202, 0.021360350772738, 0.021088203415275, 0.020816056057811, 0.020543908700347, 0.020271761342883 ]

// and the differences between the reset points
r.collect(_.first).differentiate
-> [ 0.0, 0.022448940202594, -0.00027214735746384, -0.00027214735746384, -0.00027214735746384, -0.00027214735746384, -0.00027214735746384, -0.00027214735746384, -0.00027214735746384, -0.00027214735746384 ]

On every cycle, the Phasor ramp is shifting down slightly, by the same amount every time. This is correct for modulo with a constant increment. (At a different frequency, it may shift up slightly.) If we do the same thing with integers, let’s say, increment = 3, upper limit = 8 (8/3 isn’t an integer), then: 0, 3, 6 // 1, 4, 7 // 2, 5 // 0, 3, 6 etc. – each “cycle” is up-one (and another interesting observation – the 3+3+2 pattern is related to aliasing :wink: ). It would be wrong to do a +3 cycle over 8 as 0, 3, 6, 0, 3, 6, 0, 3, 6 – this involves a discontinuity every 3 samples.

By contrast, with Sweep and a trigger:

p = {
	var phase = Sweep.ar(Impulse.ar(1002), 1002);
	[phase, SinOsc.ar(0, phase * 2pi)]
}.plot;

q = p.value[0];
r = q.separate { |a, b| b < a };

r.collect(_.first);
-> [ 0.022721087560058, -0.0, -0.0, -0.0, -0.0, -0.0, -0.0, -0.0, -0.0, -0.0 ]

r.collect(_.last);
-> [ 1.022449016571, 0.97700679302216, 0.97700679302216, 0.97700679302216, 0.97700679302216, 0.97700679302216, 0.97700679302216, 0.97700679302216, 0.97700679302216, 0.97700679302216 ]

Except for a glitch in the first cycle (which could be a UGen init bug), the endpoints of each cycle are exactly the same… but if the wavelength is not an integer number of samples, then there is no way that this is correct. That means there is already a minor discontinuity in the sine wave – it might not be enough to notice in this basic case, but when you crank up the FM, that might make it noticeable.

Incidentally, the Phasor trigger is useful for implementing oscillator hard-sync! But you’re not looking for oscillator sync here.

I guess one other take away from this is about streamlining and simplifying code. Usually(?) Often(?) the best solution is the one that has removed as much unnecessary stuff as possible.

hjh

2 Likes