Granular Delay [How to]

I would like to implement a granular delay but I don’t have any physical one to evaluate what exactly they are doing. I have come across these videos:

After watching Eli Fieldsteel videos I think that the granulation part is quite straightforward, but is there any trick for implementing the delay? Should I somehow make the delay synchronized with the grain rate?

Should I have one delay unit for each grain? If yes, how to achieve this?

If anyone does have some boilerplate code to share it would be great.

Have you seen this video: https://www.youtube.com/watch?v=c5wM-Pgxf70?

1 Like

This is probably quite unorthodox, but i use this synthdef all the time for my multichannel granular delay mangling:

(
SynthDef(\graincloud, {
    arg in = 0, out = 0, maxGrains = 64;
    var numChannels = 13;
    var input, circularBufs, writePos, bufFrames;
    var buf = \bufnum.kr(-1);
    var amp = \levels.kr(1!numChannels, 1/30, fixedLag:true),
        delayTime = \delay.kr(1000!numChannels)/1000,
        grainDur = \graindur.kr(100!numChannels)/1000,
        pitch = \pitch.kr(0!numChannels),
        dryWet = \mix.kr(0.5!numChannels, 1/30, fixedLag: true),
        reverse = \reverse.kr(0!numChannels),
        feedback = \feedback.kr(0.5!numChannels);

    // Filter parameters
    var lpfCutoff = \lpf.kr(130!numChannels).midicps,
        hpfCutoff = \hpf.kr(1!numChannels).midicps;

    var trigger = \graintrig.kr(0!numChannels);
    var bufferSize = 16;
    var grainSynths, dry, wet, filteredWet, outputSignal;
    var maxPossibleDur, limitedGrainDur, effectiveRate;

    // Input
    dry = In.ar(in, numChannels);
    wet = LocalIn.ar(numChannels);

    // Apply feedback
    input = LeakDC.ar(wet * feedback + dry);

    // Circular buffer setup for each channel
    circularBufs = numChannels.collect {
        LocalBuf(SampleRate.ir * bufferSize, 1).clear;
    };
    bufFrames = BufFrames.kr(circularBufs[0]);
    writePos = Phasor.ar(0, 1, 0, bufFrames);

    // Write each channel to its own circular buffer
    numChannels.do { |i|
        BufWr.ar(input[i], circularBufs[i], writePos);
    };

    // Calculate effective rate (considering reverse)
    effectiveRate = pitch.midiratio * (1 - (2 * reverse));

    // Calculate maximum possible duration for each grain
    maxPossibleDur = delayTime / effectiveRate.abs;

    // Limit grain duration to prevent overpassing write position
    limitedGrainDur = grainDur.clip(0, maxPossibleDur);

    // Polyphonic grain synthesis for each channel
    grainSynths = numChannels.collect { |i|
        var grainPos = Demand.kr(trigger[i], 0,
            (writePos - (Demand.kr(trigger[i], 0, delayTime[i]) * SampleRate.ir)) / bufFrames
        );

        GrainBufJ.ar(
            numChannels: 1,
            trigger: trigger[i],
            dur: Demand.kr(trigger[i], 0, limitedGrainDur[i]),
            sndbuf: circularBufs[i],
            rate: Demand.kr(trigger[i], 0, effectiveRate[i]),
            pos: grainPos,
            interp: 2,
            envbufnum: buf,
            maxGrains: maxGrains
        );
    };

    // Apply LPF and HPF in series to the wet (granular) signal for each channel
    filteredWet = numChannels.collect { |i|
        var sig = grainSynths[i];
		sig = HPF.ar(sig, hpfCutoff[i]);
        sig = LPF.ar(sig, lpfCutoff[i]);
        sig;
    };

    // Mix dry and filtered wet signals for each channel
    outputSignal = numChannels.collect { |i|
        XFade2.ar(dry[i], filteredWet[i], dryWet[i] * 2 - 1);
    };

    // Send processed and filtered signal back for feedback
    LocalOut.ar(filteredWet);

    // Output all channels
    Out.ar(out, outputSignal * amp);
}).add();
)

You just need to give it a grain envelope buffer (or simply use -1 for envbufnum to use the default hanning windowing), as the circular buffer for the delay is done using localbuff

1 Like

Thanks a lot!!!

Why exactly are you using GrainBufJ instead of GrainBuf? Is it because GrainBufJ has a loop argument, so instead of using a Phasor with automatic loop you will use an EnvGen to retrigger the buffer playback during the middle of the playback?

The usage for this code is when you have, let’s say, 8 independent channels of audio inside SC and you would like to display them in a loudspeaker array of 8 loudspeakers? 8 independent granulators one going exactly to a single independent speaker?

It is not intended for having a stereo input and splaying the grains across a ring of 8 loudspeakers, right?

Hey John!

To be honest, i don’t remember why i used grainbugj… it was some time ago and lost track of the development process. But as far as i can see, it could be easily swapped by the vanilla grainbuf and it wont change the functionality at all, as its not using the loop functionality at al. Great observation!

And yes, i usually play with multichannel setups and have one granulator per channel, hence the multichannel expansion in all named controls, to be able to have independent control over parameters on per-channel basis. In some situations i chain my synths with other effect processor synthdefs to move positions of each channel, or even to downmix to stereo if working just with LR and use the 13channels as a metaphor of 13 independent voices…

I work mainly in the art installation field, and all this multichannel goodies are a must our creative practice. you can take a look at what we do here: https://www.playmodes.com/

If useful for anything, you can find my collection of multichannel synthdefs here: GitHub - PlaymodesStudio/oceanodeSynthdefs: A repository to save the .scd files with the synthdefs used for the oceanode framework. Might be reusable for other environments like Tidal or SonicPi
Many weird and unorthodox things there, but there’s lot of work done and hopefully it can be useful for somone else

1 Like

Amazing work!!! Congrats!!

Really interesting stuff, thanks a lot for sharing!!

1 Like