Advanced Synthesis - Oscillator Sync

Does any one know why im getting clicks when modulating the master oscillator?.

Im basically working on resonant waveforms by doing hard sync, the main idea is to have one oscillator that syncs the slave and another oscillator that is at the same frequency of the master that will modulate the amplitude of the slave, so when the discontinuities of the hard sync arrived they will have 0 amplitude, thus it will sound smooth, like this:

Everything seems to be woking fine, but at some moments it kind of lags or something when you modulate the master frequency and it gets out of sync in a really tiny window, so in the scope you can see the discontinuity and also you can hear a click.

I would really appreciate some help, I tried so many things but apparently it has to be with a lag between the sever and the client when modulating the master frequency.

Just try to hear some rounds, it doesn’t click all the time, just at the lowest frequencies.

//Creation of the sine wave from 0 to 1

(
var numFrames = 2048;
var sig = (Signal.sineFill(numFrames, [1], [-pi/2]) +1)*0.5;
b = Buffer.alloc(s,numFrames);
b.loadCollection(sig);
d = Buffer.loadCollection(s, Scale.harmonicMinor);
b.plot
)

//Hard sync resonance

(
{
	var baseFreq, playbackRate, follower, followerFreq, leader, leaderFreq, ampMod, sig, trig;
	trig = Impulse.ar(5);
	baseFreq = s.sampleRate / (b.numFrames);
	leaderFreq = SinOsc.ar(0.1).range(1,100);
	leader = Impulse.ar(leaderFreq);
	followerFreq = leaderFreq + 10;
	playbackRate = (followerFreq) / baseFreq;
	ampMod = EnvGen.ar(Env.perc(0, 1, curve:0), leader, timeScale:1 / leaderFreq;);
	follower = PlayBuf.ar(1, b, playbackRate, trigger: leader , loop: 1) ;
	sig =(follower * ampMod)!2 * 0.2;
}.play;
)

Also if I modulate just the slave frequency and let the master frequency constant it works as a resonant waveform.

(
{
	var baseFreq, playbackRate, follower, followerFreq, leader, leaderFreq, ampMod, sig, trig;
	trig = Impulse.ar(5);
	baseFreq = s.sampleRate / (b.numFrames);
	leaderFreq = 40;
	leader = Impulse.ar(leaderFreq);
	followerFreq = leaderFreq + SinOsc.ar(0.1, -pi/2).range(0,1000);
	playbackRate = (followerFreq) / baseFreq;
	ampMod = EnvGen.ar(Env.perc(0, 1, curve:0), leader, timeScale:1 / leaderFreq;);
	follower = PlayBuf.ar(1, b, playbackRate, trigger: leader , loop: 1) ;
	sig =(follower * ampMod)!2 * 0.2;
}.play;
)

But as soon as I try to modulate both, it does really weird stuff:

(
{
	var baseFreq, playbackRate, follower, followerFreq, leader, leaderFreq, ampMod, sig, trig;
	trig = Impulse.ar(5);
	baseFreq = s.sampleRate / (b.numFrames);
	leaderFreq = SinOsc.ar(0.1, -pi/2).range(1,100);
	leader = Impulse.ar(leaderFreq);
	followerFreq = leaderFreq + SinOsc.ar(0.1, -pi/2).range(0,1000);
	playbackRate = (followerFreq) / baseFreq;
	ampMod = EnvGen.ar(Env.perc(0, 1, curve:0), leader, timeScale:1 / leaderFreq;);
	follower = PlayBuf.ar(1, b, playbackRate, trigger: leader , loop: 1) ;
	sig =(follower * ampMod)!2 * 0.2;
}.play;
)

thanks for the time.

I would strongly recommend LFTri for this, and not an EnvGen.

There should be some threads here about windowed sync – you can likely pick up some ideas from those.

hjh

yes, the thing is that the sound I want is with an inverted saw tooth, it really sounds like a filter, with a triangle sounds more smooth, not so resonant. But I couldn’t found a Ugen like that, I couldn’t make it work with phasor.

What’s wrong with the code that DK Mayer posted in post 2, or 3. I’ve used that approach many times with no problems.

I tried already to do it with BufRd but couldn’t make it sync correctly, but now that you point me to a working implementation I will try to do it again to see if it has betters results. Thanks :slight_smile:

I have been trying to implement the bandlimited saw with hardsync from the GO book in SC.
Its mostly about calculating the subsample offset of the naive phasors wrap. Some things to consider with this approach are:

The phasors ramp in discrete time has no infinite resolution. The resolution is determined by the sample rate, which is an approximation of the ideal phasor in continous time.
In the moment the phasor wraps around it doesnt make a big jump from 1 to 0 but instead moves continuously from 1 to 0 over one sample. This transition does not perfectly align with the sample rate and therefore doesnt perfectly reaches 0. These slight deviations add up with every cycle and the higher the phasors frequency the bigger the deviations for every cycle. These create irregular harmonic periods and therefore introduce unrelated frequency content into the phasors sound.

The simple idea for bandlimiting in this case is instead of trying to synthesize an ideal phasor, to synthesize a function that already has the single-sample slope requirement embedded into it. This function should be independent from the sample points and is used to place the slope precisely centered on the ideal phasors transition point rather than the sampling grid.

To do that you calculate the values of the ideal phasor one-half a sample frame before and after it has a transition and the subsample location of the ideal transition relative to the sampling grid to get the ramp between these two corner points and replace the naive phasor when it is in its “wrapping frame” with a “replacement phasor” taken from these calculations and then you add hard sync :wink:

the patch looks like this:

for a frequency of 2000 hz without hard sync it sounds like this:

raw phasor:

antialiased:

thats my current attempt, there is no switch Ugen in SC so i have used Select. The output is anti-aliased somehow but not as good as the audio examples from the gen patch above. if you have any further ideas let me know :slight_smile:

(
var rampToSlope = { |phase|
	var delta = Slope.ar(phase) * SampleDur.ir;
	delta.wrap(-0.5, 0.5);
};

var rampRotate = { |phase, offset|
	(phase - offset).wrap(0, 1);
};

var getSubSampleOffset = { |phase|
	var slope = rampToSlope.(phase);
	var sampleCount = phase - (slope < 0) / slope;
	var trig = Trig1.ar((sampleCount < 1) * slope, SampleDur.ir);
	var subSample = Latch.ar(sampleCount, trig);
	(subSample: subSample, trig: trig);
};

var accumulatorSubSample = { |trig, subSampleOffset|
	var accum = Duty.ar(SampleDur.ir, trig, Dseries(0, 1));
	accum + subSampleOffset;
};

{
	var rate, slope, syncPhasor, syncTrig, subSampleOffset, subSampleOffsetAndTrig, accumulator;
	var corePhasor, loopSubSample, mix, beforeTrans, afterTrans;
	var trig, replaceBefore, replaceAfter, sig;

	rate = \rate.kr(205);
	slope = rate * SampleDur.ir;

	syncPhasor = Phasor.ar(DC.ar(0), \syncRate.kr(60) * SampleDur.ir);

	subSampleOffsetAndTrig = getSubSampleOffset.(syncPhasor);
	subSampleOffset = subSampleOffsetAndTrig[\subSample];
	syncTrig = subSampleOffsetAndTrig[\trig];

	accumulator = accumulatorSubSample.(syncTrig, subSampleOffset);
	corePhasor = (slope * accumulator).wrap(0, 1);

	loopSubSample = corePhasor.wrap(-0.5, 0.5) + (slope * 0.5) / slope;
	mix = Select.ar(syncTrig, [loopSubSample, subSampleOffset]);

	beforeTrans = rampRotate.(corePhasor, mix * slope);
	afterTrans = rampRotate.(corePhasor, mix - 1 * slope);

	trig = (mix >= 0) * (mix < 1);

	replaceBefore = Select.ar(trig, [corePhasor, beforeTrans]);
	replaceAfter = Select.ar(trig, [corePhasor, afterTrans]);

	sig = LinXFade2.ar(replaceBefore, replaceAfter, mix.clip(0, 1));

	//sig = sig * 2 - 1;

	sig = LeakDC.ar(sig);

	sig!2 * 0.03;

}.play;
)

that beeing said i think most of this stuff is better done in C++ :slight_smile:

EDIT: keeping track of the subsample offset and resetting the phasor not to 0 but to the subsample offset location reduces aliasing for higher rates quite efficienctly, see my other thread:

granulation of a sine carrier with a hanning window and a trigger rate 1000 hz without subsample offset:
grafik

granulation of a sine carrier with a hanning window and a trigger rate 1000 hz with subsample offset:
grafik

thats one benefit of having a source of time which has a continuous slope like a phasors ramp, so you know at every time exactly what slope you have instead of having triggers without knowing when they happen or when the last time was they happened.

1 Like

Is that book worth getting if you don’t have, or know, Max? I was just looking at the table of contents and it seems kind of interesting. But don’t know if I’d get enough use out of it to justify the price.

I think the book is totally worth it. I have implemented alot of its ideas in SC. But then started to work in gen~ a few months ago to make proper Ugens out of synthesis ideas i had which often times had to be a compromise of some sort when working in SC. I think the hard sync is a good example for that.

Yeah, I didn’t quite follow what you wrote above (I think I’d have to get the book) but I’d find it easier to write a UGen that implemented that in C++. It might be possible in SuperCollider, but it would take me less time in C++ and the result would be faster/more useful.

That phasor seems like it could be useful, and the table of contents are intriguing. I’ll probably buy the book…

im not familiar with C++ and wanted to make a smooth step with DSP into that. So starting with gen and have the ability to export C++ code via RNBO was already a good start. Have created the rung divisions eurorack module as a first project in gen and will probably port that to SC.

The second book will come out in spring / summer, really looking forward to its anti-aliasing content.
table of contents:

3 Likes

Excuse my lack of knowledge, but what is gen~, another framework?

its max msps single sample processing enviroment.

1 Like

can you make a sweep of the hard sync freq to hear how the aliased free version develop over time?

Do you mean with the gen patch ?

yes, the one that does not have alias, I liked your approach but I would like to hear the whole frequency range please.

thanks

here with a carrier of 440 hz and sweeping the follower up to 2000 hz.

aliased:

anti-aliased:

1 Like

waa!, cool, there is really a difference, thanks for the implementation :slight_smile:

Haha yeah of course there is a difference :slight_smile:

Looking forward to look at your implementations!

im not familiar with C++ and wanted to make a smooth step with DSP into that.

Oh sure. It’s just that trying to implement any algorithm that relies upon sample level code is always going to be a challenge in SuperCollider as it doesn’t support that very well.

You could investigate Faust. It’s a lot easier to wrap your head around and can generate UGens.