// https://nathan.ho.name/posts/pulsar-synthesis/

(
{
    var snd, freq, formantFreq, pulsaretPhase;
    freq = 100;
    formantFreq = 400;
    pulsaretPhase = LFSaw.ar(freq, iphase: 1).linlin(-1, 1, 0, 1) * formantFreq / freq;
    pulsaretPhase.dup;
}.play;
)

pulsaretPhase is then used in two ways. First, if it’s greater than or equal to 1, the pulsaret has stopped playing and silence is produced. Second, the pulsaretPhase indexes into the pulsaret signal. Here’s a sine wave pulsaret with proper gating:

(
{
    var snd, freq, formantFreq, pulsaretPhase;
    freq = 100;
    formantFreq = 400;
    pulsaretPhase = LFSaw.ar(freq, iphase: 1).linlin(-1, 1, 0, 1) * formantFreq / freq;
    snd = sin(pulsaretPhase * 2pi) * (pulsaretPhase < 1);
    snd.dup;
}.play;
)

We can generalize to multiple concatenated sine waves by multiplying the term inside the sin(...) by a value we’ll call sineCycles:

(
{
    var snd, freq, formantFreq, sineCycles, pulsaretType, pulsaretPhase;
    freq = 100;
    formantFreq = 400;
    sineCycles = 4;
    pulsaretPhase = LFSaw.ar(freq, iphase: 1).linlin(-1, 1, 0, 1) * formantFreq / freq;
    snd = sin(pulsaretPhase * 2pi * sineCycles.floor) * (pulsaretPhase < 1);
    snd.dup;
}.play;
)

We can apply an approximately exponentially decaying window to the pulsaret, as opposed to the rectangular one we’ve been using so far:

(
{
    var snd, freq, formantFreq, sineCycles, pulsaretPhase, window;
    freq = 100;
    formantFreq = 200;
    sineCycles = 4;
    pulsaretPhase = LFSaw.ar(freq, iphase: 1).linlin(-1, 1, 0, 1) * formantFreq / freq;
    window = pulsaretPhase.lincurve(0, 1, 1, 0, -4);
    snd = sin(pulsaretPhase * 2pi * sineCycles.floor) * window * (pulsaretPhase < 1);
    snd.dup;
}.play;
)

That’s enough plotting. Let’s hear it with random modulation of all the parameters we discussed, including amplitude and pan position:

(
{
    var snd, freq, formantFreq, sineCycles, pulsaretPhase, window, randomLFO;
    randomLFO = { LFNoise2.kr(5) };
    freq = randomLFO.().linexp(-1, 1, 1, 1000);
    formantFreq = randomLFO.linexp(-1, 1, 2, 8000);

    sineCycles = randomLFO.().linlin(-1, 1, 1, 4);
    pulsaretPhase = LFSaw.ar(freq, iphase: 1).linlin(-1, 1, 0, 1) * formantFreq / freq;
    window = pulsaretPhase.lincurve(0, 1, 1, 0, -4);
    snd = sin(pulsaretPhase * 2pi * sineCycles.floor) * window * (pulsaretPhase < 1);
    snd = snd * randomLFO.().linlin(-1, 1, 0, 1);
    snd = Pan2.ar(snd, randomLFO.() * 0.4);
    snd = snd * -5.dbamp;
    snd ! 2;
}.play(fadeTime: 0);
)

As discussed in Microsound, let’s use three separate formants and pan them around the stereo field separately:

(
{
    var snd, freq, formantCount, formantFreq, sineCycles, pulsaretPhase, window, randomLFO, randomLFOs;
    formantCount = 3;
    randomLFO = { LFNoise2.kr(5) };
    randomLFOs = { { randomLFO.() } ! formantCount };
    freq = randomLFO.().linexp(-1, 1, 1, 1000);
    formantFreq = randomLFOs.().linexp(-1, 1, 2, 8000);
    sineCycles = randomLFOs.().linlin(-1, 1, 1, 4);
    pulsaretPhase = LFSaw.ar(freq, iphase: 1).linlin(-1, 1, 0, 1) * formantFreq / freq;
    window = pulsaretPhase.lincurve(0, 1, 1, 0, -4);
    snd = sin(pulsaretPhase * 2pi * sineCycles.floor) * window * (pulsaretPhase < 1);
    snd = snd * randomLFOs.().linlin(-1, 1, 0, 1);
    snd = Pan2.ar(snd, randomLFOs.() * 0.4);
    snd = snd.flop.sum;
    snd = snd * -2.dbamp;
    snd;
}.play(fadeTime: 0);
)

This gets old fast, and the shapes of the random LFOs are the culprit. They are all constantly moving smoothly and there’s a lack of space to make movement sound special, leading to monotony. Recalling the switching method that I talked about in my last blog post, let’s have each random LFO produce a random static value every now and then. Also, let’s modulate the rate of all LFOs with another global slow LFO that determines their overall rate:

(
{
    var snd, freq, formantCount, formantFreq, sineCycles, pulsaretPhase, window, randomLFO, randomLFOs, lfoRate;
    formantCount = 3;
    lfoRate = LFDNoise1.kr(0.3).linexp(-1, 1, 0.1, 16);
    randomLFO = {
        var trigger;
        trigger = Dust.kr(lfoRate);
        Select.kr(ToggleFF.kr(trigger), [
            LFNoise2.kr(lfoRate),
            TRand.kr(-1, 1, trigger)
        ]);
    };
    randomLFOs = { { randomLFO.() } ! formantCount };
    freq = randomLFO.().linexp(-1, 1, 1, 1000);
    formantFreq = randomLFOs.().linexp(-1, 1, 2, 8000);
    sineCycles = randomLFOs.().linlin(-1, 1, 1, 4);
    pulsaretPhase = LFSaw.ar(freq, iphase: 1).linlin(-1, 1, 0, 1) * formantFreq / freq;
    window = pulsaretPhase.lincurve(0, 1, 1, 0, -4);
    snd = sin(pulsaretPhase * 2pi * sineCycles.floor) * window * (pulsaretPhase < 1);
    snd = snd * randomLFOs.().linlin(-1, 1, 0, 1);
    snd = Pan2.ar(snd, randomLFOs.() * 0.4);
    snd = snd.flop.sum;
    snd = snd * -2.dbamp;
    snd;
}.play(fadeTime: 0);
)

As always, it’s not just the synthesis recipe, it’s also how you modulate it.

To bring this patch into the 21st century, we can stack on a few effects. Here are three pitch shifters (not just any pitch shifter, but the weird and metallic PitchShift) and three frequency shifters, all modulated randomly:

(
{
    var snd, freq, formantCount, formantFreq, sineCycles, pulsaretPhase, window, randomLFO, randomLFOs, lfoRate;
    formantCount = 3;
    lfoRate = LFDNoise1.kr(0.3).linexp(-1, 1, 0.1, 16);
    randomLFO = {
        var trigger;
        trigger = Dust.kr(lfoRate);
        Select.kr(ToggleFF.kr(trigger), [
            LFNoise2.kr(lfoRate),
            TRand.kr(-1, 1, trigger)
        ]);
    };
    randomLFOs = { { randomLFO.() } ! formantCount };
    freq = randomLFO.().linexp(-1, 1, 1, 1000);
    formantFreq = randomLFOs.().linexp(-1, 1, 2, 8000);
    sineCycles = randomLFOs.().linlin(-1, 1, 1, 4);
    pulsaretPhase = LFSaw.ar(freq, iphase: 1).linlin(-1, 1, 0, 1) * formantFreq / freq;
    window = pulsaretPhase.lincurve(0, 1, 1, 0, -4);
    snd = sin(pulsaretPhase * 2pi * sineCycles.floor) * window * (pulsaretPhase < 1);
    snd = snd * randomLFOs.().linlin(-1, 1, 0, 1);
    snd = Pan2.ar(snd, randomLFOs.() * 0.4);
    snd = snd.flop.sum;
    [0.2, 0.1, 0.05].do { |windowSize|
        snd = PitchShift.ar(snd, windowSize, randomLFO.().linexp(-1, 1, 0.5, 2)) * 6.dbamp;
        snd = FreqShift.ar(snd, randomLFO.() * 100);
    };
    snd = Limiter.ar(snd);
    snd = snd * -2.dbamp;
    snd;
}.play(fadeTime: 0);
)



