Advanced Synthesis - Oscillator Sync

So the idea is to start a series of posts reimplementing the ideas discussed in the Advanced Programming Techniques for Modular Synthesis. Each post will focus on a chapter, or part of a chapter.

A big motivation for me doing this is that I want to learn stuff. There’s a good chance that things I post will be wrong, or not the best approach. I just see that as part of the learning process. Feel free to correct, or improve anything I post. Or just take these ideas and do something cool. That’s kind of the point.

In this thread I’m going to start with oscillator sync. This might be the worst place to start as SuperCollider doesn’t really do a good job of supporting sync. None of the standard oscillators allow synching, so you have to hack it. There is a SyncSaw Ugen in extras, but it aliases. There are solutions to at least some of these problems though.

Here are a couple of links on what Oscillator Sync are as background:

And now here are the Nord Modular Links:

4 Likes

you have no idea how here for this I am.

I’ll be following closely, probably with plenty of questions

Hi,

there are two old threads about this.

https://www.listarc.bham.ac.uk/lists/sc-users-2015/msg47046.html

https://www.listarc.bham.ac.uk/lists/sc-users-2012/msg20757.html

You can do it with these specific UGens which have phase input + Phasor (also in variants), besides Osc or BufRd are possible for arbitrary waveforms. Personally I prefer BufRd to Osc to avoid the wavetable format.

All this is really hard sync with aliasing in general therefore

Example with BufRd

(
// use chebyshev polynomial as waveform
a = Signal.chebyFill(512, 0!5 ++ 1);
b = Buffer.loadCollection(s, a);
a.plot
)

s.freqscope

(
x = { |oscFreq = 100, trigFreq = 172|
    var trig = Impulse.ar(trigFreq);
    BufRd.ar(1, b, Phasor.ar(trig, oscFreq * 512 / SampleRate.ir, 0, 511), 1, 4) * 0.03
}.play
)

// change fundamental

x.set(\trigFreq, 145)

x.set(\trigFreq, 270)

x.set(\trigFreq, 60)


// change spectrum

x.set(\oscFreq, 205)

x.set(\oscFreq, 75)

x.release


// you can reduce artefacts with Lag e.g., a bit handwoven but might be practical

s.freqscope

// change lagtime with MouseX

(
y = { |oscFreq = 100, trigFreq = 109|
    var trig = Impulse.ar(trigFreq);
    Lag.ar(
        BufRd.ar(1, b, Phasor.ar(trig, oscFreq * 512 / SampleRate.ir, 0, 511), 1, 4),
        MouseX.kr(0.00003, 0.0003).poll(2, label: \lag)
    ) * 0.03
}.play
)

y.release

It might also be possible to modify Filter / VarLag params in parallel to the phasor reset, but I haven’t played around with that, it could maybe also make the filter instable in extreme cases.

James McCartney mentions the MinBLEPs technique, which uses sinc impulses. This is an elaborated procedure though.

Regards

Daniel

3 Likes

So let’s start with the easiest way to do this. In extras there’s a SyncSaw ugen that handles synching for us. Unfortunately it’s not anti-aliased, so let’s get a sense of what that means. First of all let’s fire up a frequency scope:

FreqScope.new();

Then play the synth below and switch between buses 0 and 1 on the frequency scope:

{[LFSaw.ar(1000), Saw.ar(1000)]}.play

So with the aliasing version on bus 0, and the anti-aliased version on bus 1 you should be able to see (and maybe hear) the difference. The aliasing is pretty bad. We can improve things a little:

{[LFSaw.ar(1000), HPF.ar(LFSaw.ar(1000), 900), Saw.ar(1000)]}.play

Bus 1 is better, but we still have a lot of noise between the fundamentals. So this is far from perfect. Let’s not worry about that for the moment. Evaluate the following function:

(
/* fFreq = Fundamental Frequency
   sFreq = Saw Frequency
*/
f = {|fFreq, sFreq, cycles=5|
  var x = {[
    SyncSaw.ar(fFreq, sFreq),
    LFSaw.ar(fFreq).range(-0.5, 0.5),
  ]};
  x.plot(cycles/fFreq);
}
)

This function will plot the output of the syncsaw on the top graph, with a reference saw wave below. This will give you a sense of t

fFreq is the frequency of the sync wave and sFreq is the frequency of our output saw wave. So our saw wave is being reset fFreq times per second, but the saw wave’s frequency is sFreq. Too much theory, let’s draw stuff:

f.(100, 100);

So this just a saw wave. Our sync oscillator is resetting our saw wave when our saw wave reaches a phase of 0. Let’s try some more values:

f.(100, 200);

If you ignore the second wave which is some kind of weird artefact, you can see that we have doubled the frequency.

f.(100, 300);

and so on. Let’s do something more interesting:

f.(100, 150);

(again ignore the second wave). So this time our periodic frequency is unchange, but our wave is split into two waves. The first saw wave which has the frequency of our saw wave, the second wave which has a smaller frequency with a range of -1 to 0.

f.(100, 250);

and let’s look at the frequency components:

{SyncSaw.ar(100, 200)}.play

So our fundamental here has been shifted (as we’d expect) from the saw wave (remember to do cmd-period/ctrl-period each time):

{Saw.ar(100)}.play

Playing around a bit more:

  f.(100, 120)
  {SyncSaw.ar(100, 200)}.play

So essentially when we double the saw frequency, we double the fundamental of the overall wave, which makes sense.

Whereas the faction changes the timbre, with fractions closer to the next number shifting the energy into the next harmonic:

  f.(100, 120)
  {SyncSaw.ar(100, 120)}.play
  
  f.(100, 280)
  {SyncSaw.ar(100, 280)}.play

So whole number ratios shift us up an octave, fractional values change the timbre (though inevitably higher fractions will also shift the perceived tone). I suspect, though I haven’t checked, that the more imperfect the ratio between the fundamental and audio wave frequencies, the wilder the timbre. Feel free to check.

How about we cross the streams:

  f.(300, 227.3)

So there’s nothing stopping us doing this, it’s just fairly uninteresting. We’re just reducing the amplitude and DC biasing our signal. So when doing hard sync always make the /fundamental/ lower than the audio wave. And when working out the affect remember:

  • fundamental defines the fundamental note.
  • The ratio of the audio wave frequency to the audio wave frequency defines the note we hear and the timbre.

So if we have a fundamental of 100hz and a saw wave of 243hz. the output wave will have a frequency of 200 hz, and and the 43 hz will affect our timbre.

That’s part one. Feel free to tell me what’s wrong and I’ll correct it. Part two will come whenever I find time and will start looking at some audio applications and ways to implement it in supercollider.

Daniel. Thanks for that! Those were certainly the approaches I was going to investigate but actual working code is great! Nathan also sent me some stuff on Slack that I’m digesting. Ultimately if this works I’d like to package these up into something more formal/document like. But let’s see how this goes for the moment.

I don’t think miniBleps would be possible directly in SuperCollider. Would need a UGen I think. I do have an idea for creating a anti-aliased syncable oscillator though. Think it’s doable and would be a neat thing.

Yes, probably.

That would definitely be nice!

Good stuff!
I got a few G2 patches that would be interesting to see come alive in SuperCollider
might be a good challenge

btw, that Nick Collins tutorial on server side sequencing you referred me to earlier had a surprisingly pleasing example of hard sync that uses an envelope generator:

(
{
	var mouse_y, mouse_x;
	mouse_y = MouseY.kr (0, 1) * [ 0, 128, 256, 128 ] / SampleRate.ir;
	mouse_x = MouseX.kr (10, 300, 'exponential');
	EnvGen.ar( Env([ 0, 0, 1, -1, 0 ], mouse_y), Impulse.ar (mouse_x));
}.play
)

minBLEP has been mostly superseded by polyBLEP. it’s tricky, but i think you could do polyBLEP using just existing UGens.

Nathaniel Virgo put up an anti-aliased (ish) SyncSaw on SCcode which I’ll post up here when I’ve cleaned it up (think it would be better as a pseudo ugen, with a few other fixes). He uses a buffer to implement delay lines which he uses to integrate a Saw wave. It sounds good, which I guess is the important test for these things.