Skip to main content

Pulsar Synthesis

Curtis Roads’ book Microsound is a must-read for any nerdy music technologist. Despite its age, it contains many exciting techniques under the granular synthesis umbrella that still sound fresh and experimental. I recently started exploring one of these methods, called pulsar synthesis, and thought it’d be fun to write my own review of it and demonstrate how to accomplish it in SuperCollider or any similar environment.

Pulsar synthesis produces periodic oscillations by alternating between a short, arbitrary waveform called the pulsaret (after “wavelet”) and a span of silence. The reciprocal of the pulsaret’s length is the formant frequency, which is manipulated independently of the fundamental frequency by changing the speed of the pulsaret’s playback and adjusting the silence duration to maintain the fundamental. Roads calls this “pulsaret-width modulation” or PulWM. If the formant frequency dips below the fundamental frequency, the silence disappears and the pulsarets can either be truncated (regular PulWM) or overlapped (overlapped PulWM or OPulWM). Roads describes OPulWM as “a more subtle effect” due to phase cancellation.

The pulsaret signal can be anything. The book suggests the following, among others: a single sine wave cycle, multiple sine wave periods concatenated, and a bandlimited impulse (sinc function). Another option is to take the waveform from live input, transferring the timbre of input over to a quasiperiodic signal and behaving slightly more like an effect than a synth.

Actual pulsars are neutron stars that emit beams of light out of their magnetic poles while spinning rapidly. As it spins, a pulsar with just the right orientation will shine a beam on Earth repeatedly and look like a periodic flashing, which can happen in a wide frequency range with periods often ranging from seconds to milliseconds. The signal they produce is not on-off. As the beam swings by us, the perceived light increases in intensity, reaches a peak, and decreases to effectively zero.

Pulsarets are usually oscillatory signals rather than the more bell-shaped signals that real pulsars produce. We can make pulsarets more accurately reflect pulsar optics using pointwise (sample by sample) multiplication of the pulsaret with a window function whose length is identical to that of the pulsaret. Suggested window functions include rectangular, Gaussian, linearly decreasing or increasing, and exponentially decreasing or increasing. The window function can even itself be oscillatory, resulting in frequency-domain convolution with the pulsaret.

Roads emphasizes an important aspect of pulsar synthesis: the fundamental frequency can venture below 20 Hz to create a train of individually audible pulses, blurring the line between rhythm and pitch.

A single pulsar synthesizer reads from the following parameters, which may vary over time: fundamental frequency, formant frequency, pulsaret waveform, window, output amplitude, and output panning.

Advanced pulsar synthesis

Roads and Alberto de Campo’s graphical program PulsarGenerator, originally developed in 1991 using Synth-O-Matic (an early SuperCollider predecessor), is described in detail in Microsound. In the development of this software, several new concepts were added to pulsar synthesis.

One way to command more exciting timbres is to use multiple pulsar synthesizers added together, with the sole constraint that they all share fundamental frequency so they fuse into one tone. PulsarGenerator used three such pulsar synthesizers. Roads notes the possibility of spatializing individual formants separately from each other.

Another trick for rattly, malfunctioning sounds is to make a pulsar synthesizer output pulsarets intermittently, in a scheme known as pulsaret masking. This can be a regular process where e.g. three pulsarets are emitted followed by two periods of silence (burst masking), or a random process where e.g. each pulsaret has a 10% chance of being replaced with silence (stochastic masking). Finally, channel masking randomly decides which channel each pulsaret is sent to according to a probability table, and these channels are spatialized.

Roads suggests convolving pulsar synthesis output with short recorded audio samples. Using bird calls and other animals as source material, he employed this technique in the second movement, “Organic,” of his work Clang-Tint (1991-94), as well as several other works. Roads additionally recommends using percussion samples for convolution.

Building a pulsar synthesis patch in SuperCollider

In February 2022, Marcin Pietruszewski published nuPG (New Pulsar Generator), which is compatible with recent versions of SuperCollider 3. If you want an out-of-the-box pulsar synthesis platform wrapped up in a nice GUI, then you know where to click.

It’s also not too hard to roll your own pulsar synthesis in SuperCollider with some math. The first step is to introduce a signal pulsaretPhase which is 0 at the beginning of the pulsaret, 1 at the end, and continues increasing during silence until the period resets. This can be created using an LFSaw that ramps from 0 to formantFreq / freq at the frequency freq:

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

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;
}.plot(0.02);
)

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;
}.plot(0.02);
)

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;
}.plot(0.02);
)

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 = randomLFOs.().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);
)

The result certainly sounds varied and unpredictable to me:

Here’s another snippet I made also using pulsar synthesis, various effects, and heavy random modulation of everything:

This time I used a downward sine wave chirp for the pulsaret, synthesizing something akin to the “moisture bass” concept we discussed a while back, and when the pulsar synthesizer reaches low frequencies the individual pulsarets sound like 909-style kick drums. The chirp qualities are audible even through the many effects, which should allay any concerns that pulsar synthesis is unnecessary and interchangeable with a “simpler” synthesis method like subtractive.

There are also two identical and parallel copies of the pulsar synthesizer and effects chain, both of which are wired to a vocoder with one signal as the carrier and the other as a modulator. This was inspired by Roads’ use of convolution (ignore the fact that vocoding isn’t really convolution), and by applying this wobbly dynamic EQ to the carrier signal a rougher and more unstable sound is achieved, not unlike a low-bitrate MP3 at times.

Acknowledgements

Thanks to dietcv on the SuperCollider forums, who reminded me of pulsar synthesis and inspired this post.