Granular Delay [How to]

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